This is page 1 of 2. Use http://codebase.md/raohwork/forgejo-mcp?page={x} to view the full context.
# Directory Structure
```
├── .env
├── .env.local
├── .forgejo
│ └── workflows
│ ├── push.yml
│ └── release.yml
├── .rmi-work
│ ├── .gitignore
│ └── conf.zsh
├── CLAUDE.md
├── cmd
│ ├── http.go
│ ├── lib.go
│ ├── root.go
│ └── stdio.go
├── Dockerfile
├── features.md
├── features.tw.md
├── fj11
│ ├── .gitignore
│ ├── app.pid
│ ├── custom
│ │ └── conf
│ │ └── app.ini
│ ├── data
│ │ ├── actions_artifacts
│ │ │ └── .gitignore
│ │ ├── attachments
│ │ │ └── .gitignore
│ │ ├── avatars
│ │ │ ├── 2c3af2cf0fdad574d90516129e2781e1
│ │ │ └── 55502f40dc8b7c769880b10874abc9d0
│ │ ├── home
│ │ │ └── .gitconfig
│ │ ├── indexers
│ │ │ └── issues.bleve
│ │ │ ├── index_meta.json
│ │ │ ├── rupture_meta.json
│ │ │ └── store
│ │ │ └── root.bolt
│ │ ├── jwt
│ │ │ └── private.pem
│ │ ├── lfs
│ │ │ └── .gitignore
│ │ ├── packages
│ │ │ └── .gitignore
│ │ ├── queues
│ │ │ └── common
│ │ │ ├── 000002.ldb
│ │ │ ├── 000005.log
│ │ │ ├── CURRENT
│ │ │ ├── CURRENT.bak
│ │ │ ├── LOCK
│ │ │ ├── LOG
│ │ │ └── MANIFEST-000006
│ │ ├── repo-archive
│ │ │ └── .gitignore
│ │ ├── repo-avatars
│ │ │ └── .gitignore
│ │ └── sessions
│ │ ├── 0
│ │ │ └── f
│ │ │ └── 0ffc8d4b843857d9
│ │ └── a
│ │ └── c
│ │ └── acefbc7d6003eb02
│ ├── forgejo-repositories
│ │ ├── .gitignore
│ │ └── test
│ │ └── test-empty.git
│ │ ├── config
│ │ ├── description
│ │ ├── git-daemon-export-ok
│ │ ├── HEAD
│ │ ├── hooks
│ │ │ ├── applypatch-msg.sample
│ │ │ ├── commit-msg.sample
│ │ │ ├── fsmonitor-watchman.sample
│ │ │ ├── post-receive
│ │ │ ├── post-receive.d
│ │ │ │ └── gitea
│ │ │ ├── post-update.sample
│ │ │ ├── pre-applypatch.sample
│ │ │ ├── pre-commit.sample
│ │ │ ├── pre-merge-commit.sample
│ │ │ ├── pre-push.sample
│ │ │ ├── pre-rebase.sample
│ │ │ ├── pre-receive
│ │ │ ├── pre-receive.d
│ │ │ │ └── gitea
│ │ │ ├── pre-receive.sample
│ │ │ ├── prepare-commit-msg.sample
│ │ │ ├── proc-receive
│ │ │ ├── proc-receive.d
│ │ │ │ └── gitea
│ │ │ ├── push-to-checkout.sample
│ │ │ ├── update
│ │ │ ├── update.d
│ │ │ │ └── gitea
│ │ │ └── update.sample
│ │ ├── info
│ │ │ ├── exclude
│ │ │ └── refs
│ │ └── objects
│ │ └── info
│ │ └── packs
│ ├── forgejo.db
│ └── lfs
│ └── .gitignore
├── glama.json
├── go.mod
├── go.sum
├── LICENSE
├── logo.svg
├── main.go
├── memo.md
├── prompt.tw.md
├── proposal.md
├── proposal.tw.md
├── README.md
├── README.tw.md
├── swagger.v1.json
├── tools
│ ├── action
│ │ ├── doc.go
│ │ └── list.go
│ ├── client_actions.go
│ ├── client_issue_attachments.go
│ ├── client_issue_dependencies.go
│ ├── client_test.go
│ ├── client_wiki.go
│ ├── client.go
│ ├── doc.go
│ ├── helpers.go
│ ├── issue
│ │ ├── attach.go
│ │ ├── comment.go
│ │ ├── crud.go
│ │ ├── dep.go
│ │ ├── doc.go
│ │ └── label.go
│ ├── label
│ │ ├── crud.go
│ │ └── doc.go
│ ├── milestone
│ │ ├── crud.go
│ │ └── doc.go
│ ├── module.go
│ ├── pullreq
│ │ ├── create.go
│ │ ├── doc.go
│ │ ├── list.go
│ │ └── view.go
│ ├── release
│ │ ├── attach.go
│ │ ├── crud.go
│ │ └── doc.go
│ ├── repo
│ │ ├── doc.go
│ │ └── list.go
│ └── wiki
│ ├── crud.go
│ ├── doc.go
│ └── list.go
└── types
├── action_test.go
├── actions.go
├── attachments.go
├── common.go
├── dependencies_test.go
├── dependencies.go
├── doc.go
├── issue_test.go
├── issues.go
├── label_test.go
├── labels.go
├── milestone_test.go
├── milestones.go
├── pullrequests.go
├── release_test.go
├── releases.go
├── repo_test.go
├── repositories.go
├── test_helpers.go
├── version.go
├── wiki_test.go
└── wiki.go
```
# Files
--------------------------------------------------------------------------------
/fj11/data/actions_artifacts/.gitignore:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/fj11/data/attachments/.gitignore:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/fj11/data/lfs/.gitignore:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/fj11/data/packages/.gitignore:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/fj11/data/repo-archive/.gitignore:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/fj11/data/repo-avatars/.gitignore:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/fj11/forgejo-repositories/.gitignore:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/fj11/lfs/.gitignore:
--------------------------------------------------------------------------------
```
```
--------------------------------------------------------------------------------
/fj11/.gitignore:
--------------------------------------------------------------------------------
```
/app.pid
```
--------------------------------------------------------------------------------
/.rmi-work/.gitignore:
--------------------------------------------------------------------------------
```
*
!conf.zsh
!.gitignore
```
--------------------------------------------------------------------------------
/.env.local:
--------------------------------------------------------------------------------
```
FORGEJO_TEST_TOKEN=921f4a8b24efdb8b781451166f42dd8d9ede152a
FORGEJO_TEST_SERVER=http://10.192.169.92:3000
```
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
```
GITEA_ACCESS_TOKEN=921f4a8b24efdb8b781451166f42dd8d9ede152a
FORGEJOMCP_TOKEN=921f4a8b24efdb8b781451166f42dd8d9ede152a
FORGEJOMCP_SERVER=http://127.0.0.1:3000
```
--------------------------------------------------------------------------------
/fj11/data/home/.gitconfig:
--------------------------------------------------------------------------------
```
[diff]
algorithm = histogram
[core]
logallrefupdates = true
quotePath = false
commitGraph = true
[gc]
reflogexpire = 90
writeCommitGraph = true
[user]
name = Gitea
email = [email protected]
[receive]
advertisePushOptions = true
procReceiveRefs = refs/for
[fetch]
writeCommitGraph = true
[safe]
directory = *
[uploadpack]
allowfilter = true
allowAnySHA1InWant = true
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Gitea/Forgejo MCP Server
> Turn AI into your code repository management assistant
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that enables you to manage Gitea/Forgejo repositories through AI assistants like Claude, Gemini, and Copilot.
## 🚀 Why Use Forgejo MCP Server?
If you want to:
- **Smart progress tracking**: Let AI help you track project progress and analyze bottlenecks
- **Automated issue categorization**: Automatically tag issue labels and set milestones based on content
- **Priority sorting**: Let AI analyze issue content to help prioritize tasks
- **Code review assistance**: Get AI suggestions and insights in Pull Requests
- **Project documentation organization**: Automatically organize Wiki documents and release notes
Then this tool is made for you!
## ✨ Supported Features
### Issue Management
- Create, edit, and view issues
- Add, remove, and replace labels
- Manage issue comments and attachments
- Set issue dependencies
### Project Organization
- Manage labels (create, edit, delete)
- Manage milestones (create, edit, delete)
- Repository search and listing
### Release Management
- Manage version releases
- Manage release attachments
### Other Features
- View Pull Requests
- Manage Wiki pages
- View Forgejo/Gitea Actions tasks
## 📦 Installation
### Method 1: Use docker (Recommended)
For STDIO mode, you can skip to **Usage** section.
For SSE/Streamable HTTP mode, you should run `forgejo-mcp` as server before configuring your MCP client.
```bash
docker run -p 8080:8080 -e FORGEJOMCP_TOKEN="my-forgejo-api-token" ronmi/forgejo-mcp http --address :8080 --server https://git.example.com
```
### Method 2: Install from source
```bash
go install github.com/raohwork/forgejo-mcp@latest
```
### Method 3: Download Pre-compiled Binaries
Download the appropriate version for your operating system from the [Releases page](https://github.com/raohwork/forgejo-mcp/releases).
## 🖥️ Usage
This tool provides two primary modes of operation: `stdio` for local integration and `http` for remote access.
Before actually setup you MCP client, you have to create an access token on the Forgejo/Gitea server.
1. Log in to your Forgejo/Gitea instance
2. Go to **Settings** → **Applications** → **Access Tokens**
3. Click **Generate New Token**
4. Select appropriate permission scopes (recommend at least `repository` and `issue` write permissions)
5. Copy the generated token
💡 **Tip**: For security, consider setting environment variables instead of using tokens directly in config:
```bash
export FORGEJOMCP_SERVER="https://your-forgejo-instance.com"
export FORGEJOMCP_TOKEN="your_access_token"
```
### Stdio Mode (for Local Clients)
This is the recommended mode for integrating with local AI assistant clients like Claude Desktop or Gemini CLI. It uses standard input/output for direct communication.
#### Configure Your AI Client
Using docker:
```json
{
"mcpServers": {
"forgejo": {
"command": "docker",
"args": [
"--rm",
"ronmi/forgejo-mcp",
"stdio",
"--server", "https://your-forgejo-instance.com",
"--token", "your_access_token"
]
}
}
}
```
Installed from source or pre-built binary:
```json
{
"mcpServers": {
"forgejo": {
"command": "/path/to/forgejo-mcp",
"args": [
"stdio",
"--server", "https://your-forgejo-instance.com",
"--token", "your_access_token"
]
}
}
}
```
You might want to take a look at **Security Recommendations** section for best practice.
### HTTP Server Mode (for Remote Access)
This mode starts a web server, allowing remote clients to connect via HTTP. It's ideal for web-based services or setting up a central gateway for multiple users.
Run the following command to start the server:
```bash
# with local binary
/path/to/forgejo-mcp http --address :8080 --server https://your-forgejo-instance.com
# with docker
docker run -p 8080:8080 -d --rm ronmi/forgejo-mcp http --address :8080 --server https://your-forgejo-instance.com
```
The server supports two operational modes:
- **Single-user mode**: If you provide a `--token` (or environment variable `FORGEJOMCP_TOKEN`) at startup, all operations will use that token.
```bash
forgejo-mcp http --address :8080 --server https://git.example.com --token your_token
```
- **Multi-user mode**: If no token is provided, the server requires clients to send an `Authorization: Bearer <token>` header with each request, allowing it to serve multiple users securely.
#### Client Configuration
For clients that support connecting to a remote MCP server via HTTP, you can add a configuration like this. This example shows how to connect to a server running in multi-user mode:
```json
{
"mcpServers": {
"forgejo-remote": {
"type": "sse",
"url": "http://localhost:8080/sse",
"headers": {
"Authorization": "Bearer your_token"
}
}
}
}
```
or `http` type (for Streamable HTTP, use different path in URL)
```json
{
"mcpServers": {
"forgejo-remote": {
"type": "http",
"url": "http://localhost:8080/",
"headers": {
"Authorization": "Bearer your_token"
}
}
}
}
```
If connecting to a server in single-user mode, you can omit the `headers` field.
## 🛡️ Security Recommendations
1. **Use environment variables**: Set `FORGEJOMCP_SERVER` and `FORGEJOMCP_TOKEN`, then remove `--server` and `--token` from your configuration
2. **Limit token permissions**: Only grant necessary permission scopes
3. **Rotate tokens regularly**: Update access tokens periodically
## 📋 Usage Examples
After configuration, you can use natural language in your AI assistant to manage your repositories:
```
"Show me critical bug reports of this repo on my gitea server"
"According to our discussion above, create a detailed issue about this bug, then leave a comment on the issue to describe how we will fix it."
"Give me a report about current milestone. Recent progression in particular."
"Analyze recent pull requests and tell me which ones need priority review"
```
## 🤝 Support & Contributing
- **Bug Reports**: [GitHub Issues](https://github.com/raohwork/forgejo-mcp/issues)
- **Code Contributions**: Pull Requests are welcome!
## 📄 License
This project is licensed under the [Mozilla Public License 2.0](LICENSE).
---
**Start making AI your code repository management partner!** 🚀
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
# Forgejo MCP Server
A Model Context Protocol (MCP) server that enables MCP clients to interact with Gitea/Forgejo repositories.
## Project Overview
This project provides MCP integration for managing Forgejo/Gitea repositories through MCP-compatible clients such as Claude Desktop, Continue, and other LLM applications.
### Supported Operations
- Issues (create, edit, comment, close)
- Labels (list, create, edit, delete)
- Milestones (list, create, edit, delete)
- Releases (list, create, edit, delete, manage assets)
- Pull requests (list, view)
- Repository search and listing
- Wiki pages (create, edit, delete)
- Forgejo Actions tasks (view)
### Transport Modes
- **stdio**: Standard input/output (best for local integration)
- **sse**: Server-Sent Events over HTTP (best for web apps) - *planned*
- **http**: HTTP POST requests (best for simple integrations) - *planned*
## Architecture
### Core Components
- **cmd/**: CLI application using Cobra framework
- `root.go`: Main command with global configuration
- `stdio.go`: Stdio transport mode implementation
- **types/**: Data structures and response types
- `api.go`: MCP response types wrapping Forgejo SDK types
- **main.go**: Application entry point
### Key Dependencies
- **MCP SDK**: `github.com/modelcontextprotocol/go-sdk`
* There are 3 important sub packages: `mcp`, `jsonschema` and `jsonrpc`
- **Forgejo SDK**: `codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2`
- **CLI Framework**: `github.com/spf13/cobra`
- **Configuration**: `github.com/spf13/viper`
If you need to get documentation of these packages, `go doc` should be the best bet.
### Implementation Strategy
- **🟢 SDK-based (71%)**: Use official Forgejo SDK where available
- **🟡 Custom HTTP (29%)**: Implement custom requests for unsupported features (Wiki, Actions, Issue dependencies)
## Configuration
### CLI Arguments
```bash
forgejo-mcp stdio --server https://git.example.com --token your_token
```
### Environment Variables
- `FORGEJOMCP_SERVER`: Forgejo server URL
- `FORGEJOMCP_TOKEN`: Access token
### Config File
Default location: `~/.forgejo-mcp.yaml`
Priority: CLI args > Environment variables > Config file
## Development
### Language and Style
- **Code/Comments**: English (open source project)
- **Documentation**: English (unless `.tw.md` suffix for Traditional Chinese)
### Key Files
- `proposal.tw.md`: Project requirements (Traditional Chinese)
- `features.tw.md`: Feature specifications (Traditional Chinese)
- `design.tw.md`: Architecture documentation (Traditional Chinese)
- `swagger.v1.json`: API documentation
### Response Format
- **Error responses**: Plain text
- **Success responses**: Markdown format in MCP TextContent.Text field
- **Structured data**: Dual format (markdown + JSON) for MCP compatibility
### Architecture Decisions
- Use **endpoint-based markdown formatting** rather than type-based to avoid LLM confusion
- Each data type implements `ToMarkdown()` method for reusability
- Endpoint handlers add context-specific headers and descriptions
- **Tool responses**: Use single TextContent with markdown formatting (follows MCP best practices)
- **Tool organization**: Mixed modular architecture with sub-packages for logical grouping
### Tool Architecture
The project uses a **mixed modular architecture** for organizing MCP tools:
#### Structure
```
tools/
├── module.go # ToolImpl interface definition
├── registry.go # Unified tool registration
├── helpers.go # Shared utility functions
├── issue/ # Issue-related operations
│ ├── crud.go # create/edit/delete issue
│ ├── list.go # list_issues
│ ├── comment.go # issue comments
│ ├── label.go # issue label operations
│ ├── attach.go # issue attachments
│ └── dep.go # issue dependencies
├── label/ # Label management
│ └── crud.go # all label operations
├── milestone/ # Milestone management
│ └── crud.go # all milestone operations
├── release/ # Release management
│ ├── crud.go # create/edit/delete release
│ └── attach.go # release attachments
├── pullreq/ # Pull Request operations
│ ├── list.go # list pull requests
│ └── view.go # get pull request details
├── action/ # Forgejo Actions (CI/CD)
│ └── list.go # list_action_tasks
├── wiki/ # Wiki pages
│ ├── crud.go # create/edit/delete wiki pages
│ └── list.go # list wiki pages
└── repo/ # Repository operations
└── list.go # repository listing and search
```
#### Design Principles
- **Logical grouping**: Related tools grouped in sub-packages
- **Single responsibility**: Each file handles closely related operations
- **Interface-driven**: All tools implement the `ToolImpl` interface
- **Extensibility**: Easy to add new tools and categories
- **Testability**: Each module can be independently tested
## Development Workflow
1. Test-Driven Development (TDD)
2. Red-Green-Refactor cycle
3. Human review at each step
4. Git commit after each milestone
## Useful Commands
### Go Commands
```bash
# Build the project for release
go build -o forgejo-mcp
# Check compilation error
go build ./...
# Run tests
go test ./...
# Format code
goimports -w .
# Get dependencies
go mod tidy
```
### Swagger
```
# Find definition of specified api endpoint
# .paths["/path/to/endpoint"].http_method
jq '.paths["/repos/{owner}/{repo}/labels/{id}"].patch' swagger.v1.json
# Get summary string of specified api endpoint
# .paths["/path/to/endpoint"].http_method.summary
#
# Change "summary" to "parameters" or "responses" to get params/responses
jq '.paths["/repos/{owner}/{repo}/labels/{id}"].patch.summary' swagger.v1.json
# Get referenced type definition
#
# for "$refs": "#/abc/def"
jq '.abc.def' swagger.v1.json
```
## Additional tools
- `my-git-server`: Tools to access remote git server. The repository is `ronmi/forgejo-mcp`.
- `gopls`: Go language server to query for difinitions, references, and more.
```
--------------------------------------------------------------------------------
/fj11/data/indexers/issues.bleve/rupture_meta.json:
--------------------------------------------------------------------------------
```json
{"version":4}
```
--------------------------------------------------------------------------------
/fj11/data/indexers/issues.bleve/index_meta.json:
--------------------------------------------------------------------------------
```json
{"storage":"boltdb","index_type":"scorch"}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
FROM ronmi/mingo
ADD forgejo-mcp /forgejo-mcp
ENTRYPOINT ["/forgejo-mcp"]
CMD ["stdio"]
```
--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://glama.ai/mcp/schemas/server.json",
"maintainers": [
"Ronmi"
]
}
```
--------------------------------------------------------------------------------
/.rmi-work/conf.zsh:
--------------------------------------------------------------------------------
```
loadlib golang
_RMI_DOTENV_AUTOLOAD=~/.zsh.d/lib/local/ai.env
loadlib dotenv
loadlib ai
export PATH="$PATH:${_RMI_WORK_DIR}"
```
--------------------------------------------------------------------------------
/tools/pullreq/doc.go:
--------------------------------------------------------------------------------
```go
// Package pullreq provides MCP tools for interacting with Forgejo pull requests.
//
// It includes tools for listing, retrieving, and creating pull requests.
package pullreq
```
--------------------------------------------------------------------------------
/tools/label/doc.go:
--------------------------------------------------------------------------------
```go
// Package label provides MCP tools for managing Forgejo labels.
//
// It includes tools for listing, creating, editing, and deleting labels within a repository.
package label
```
--------------------------------------------------------------------------------
/tools/milestone/doc.go:
--------------------------------------------------------------------------------
```go
// Package milestone provides MCP tools for managing Forgejo milestones.
//
// It includes tools for listing, creating, editing, and deleting milestones within a repository.
package milestone
```
--------------------------------------------------------------------------------
/tools/issue/doc.go:
--------------------------------------------------------------------------------
```go
// Package issue provides MCP tools for managing Forgejo issues and their comments.
//
// It includes tools for listing, retrieving, creating, editing, and deleting issues and comments.
package issue
```
--------------------------------------------------------------------------------
/tools/wiki/doc.go:
--------------------------------------------------------------------------------
```go
// Package wiki provides MCP tools for managing Forgejo wiki pages.
//
// It includes tools for listing, retrieving, creating, editing, and deleting wiki pages.
// These functionalities extend beyond the official Forgejo SDK.
package wiki
```
--------------------------------------------------------------------------------
/types/doc.go:
--------------------------------------------------------------------------------
```go
// Package types defines custom Go types that wrap or extend forgejo-sdk types.
//
// These types are primarily used to format Forgejo API responses into human-readable Markdown
// for consumption by MCP tools and AI assistants.
package types
```
--------------------------------------------------------------------------------
/tools/action/doc.go:
--------------------------------------------------------------------------------
```go
// Package action provides MCP tools related to Forgejo Actions.
//
// It currently implements the `list_action_tasks` tool, which allows listing
// Forgejo Actions execution tasks in a repository, extending functionality
// beyond the official Forgejo SDK.
package action
```
--------------------------------------------------------------------------------
/tools/release/doc.go:
--------------------------------------------------------------------------------
```go
// Package release provides MCP tools for managing Forgejo releases and their attachments.
//
// It includes tools for listing, creating, editing, and deleting releases, as well as
// managing release attachments (listing, creating, editing, and deleting).
package release
```
--------------------------------------------------------------------------------
/tools/repo/doc.go:
--------------------------------------------------------------------------------
```go
// Package repo provides MCP tools for interacting with Forgejo repositories.
//
// It includes tools for searching repositories, listing repositories owned by the
// authenticated user or an organization, and getting detailed information about a specific repository.
package repo
```
--------------------------------------------------------------------------------
/types/version.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
const VERSION = "dev-test"
```
--------------------------------------------------------------------------------
/memo.md:
--------------------------------------------------------------------------------
```markdown
# test forgejo server
### admin
- Account: test
- Pass: testtest
- Email: [email protected]
- token: b58144060b5fcc4c53b371fbc673883e405d8c66
### ai
- Account: testai
- Pass: testtest
- Email: [email protected]
- issue+repo token: 09f51cd303afbd823041833e7eb19b77d51f84ee
### repo
- name: test/test-empty
```
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package main
import "github.com/raohwork/forgejo-mcp/cmd"
func main() {
cmd.Execute()
}
```
--------------------------------------------------------------------------------
/tools/doc.go:
--------------------------------------------------------------------------------
```go
// Package tools provides a framework for implementing and registering Model Context Protocol (MCP)
// tools, and an extended Forgejo API client.
//
// This package defines the `ToolImpl` interface and `Register` helper function for MCP tool development.
// Its subpackages contain concrete implementations of these tools.
//
// It also offers `tools.Client`, an extension of the standard Forgejo SDK client, providing
// additional functionalities for interacting with Forgejo API endpoints not fully covered by the SDK.
package tools
```
--------------------------------------------------------------------------------
/.forgejo/workflows/push.yml:
--------------------------------------------------------------------------------
```yaml
name: Normal test
on:
push:
branches:
- master
jobs:
test:
name: Run Unit Tests
runs-on: any
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ">=1.23"
- name: Restore cache
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run test
run: go test -v ./...
```
--------------------------------------------------------------------------------
/tools/client_actions.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package tools
import (
"fmt"
"github.com/raohwork/forgejo-mcp/types"
)
// MyListActionTasks lists all Forgejo Actions tasks in a repository.
// GET /repos/{owner}/{repo}/actions/tasks
func (c *Client) MyListActionTasks(owner, repo string) (*types.MyActionTaskResponse, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", owner, repo)
var result types.MyActionTaskResponse
err := c.sendSimpleRequest("GET", endpoint, nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
```
--------------------------------------------------------------------------------
/types/common.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
// EmptyResponse represents an empty response for endpoints that don't return data
// Used by endpoints that only return status codes:
// - DELETE /repos/{owner}/{repo}/labels/{id}
// - DELETE /repos/{owner}/{repo}/milestones/{id}
// - DELETE /repos/{owner}/{repo}/releases/{id}
// - DELETE /repos/{owner}/{repo}/releases/assets/{id}
// - DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id}
// - DELETE /repos/{owner}/{repo}/issues/{index}/attachments/{attachment_id}
// - DELETE /repos/{owner}/{repo}/wiki/page/{pageName}
type EmptyResponse struct{}
// ToMarkdown renders simple success message for empty responses
// Example: *Operation completed successfully*
func (er EmptyResponse) ToMarkdown() string {
return "*Operation completed successfully*"
}
```
--------------------------------------------------------------------------------
/tools/helpers.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package tools
// Helper functions for creating pointers to basic types, primarily for use in
// constructing jsonschema.Schema objects where optional fields require pointers.
// BoolPtr creates a pointer to a bool value.
// This is useful for setting optional boolean fields in structs that will be
// serialized to JSON, such as in MCP tool definitions.
func BoolPtr(b bool) *bool {
return &b
}
// IntPtr creates a pointer to an int value.
// This is useful for setting optional integer fields in structs that will be
// serialized to JSON, such as in MCP tool definitions.
func IntPtr(i int) *int {
return &i
}
// Float64Ptr creates a pointer to a float64 value.
// This is useful for setting optional number fields in structs that will be
// serialized to JSON, such as in MCP tool definitions.
func Float64Ptr(f float64) *float64 {
return &f
}
```
--------------------------------------------------------------------------------
/tools/module.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package tools
import (
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// ToolImpl defines the interface that every tool implementation must satisfy.
// This interface standardizes how tools are defined and handled, ensuring they
// can be registered with the MCP server consistently.
//
// The generic types In and Out represent the tool's input and output data structures.
type ToolImpl[In, Out any] interface {
// Definition returns the formal MCP tool definition, including its name,
// description, and input schema.
Definition() *mcp.Tool
// Handler returns the function that contains the core logic of the tool.
// This function is executed when the tool is called by an MCP client.
Handler() mcp.ToolHandlerFor[In, Out]
}
// Register is a helper function that registers a tool implementation with the MCP server.
// It retrieves the tool's definition and handler through the ToolImpl interface
// and adds them to the server's tool registry.
func Register[I, O any](s *mcp.Server, i ToolImpl[I, O]) {
mcp.AddTool(s, i.Definition(), i.Handler())
}
```
--------------------------------------------------------------------------------
/fj11/custom/conf/app.ini:
--------------------------------------------------------------------------------
```
APP_NAME = Forgejo
APP_SLOGAN = Beyond coding. We Forge.
RUN_USER = ronmi
WORK_PATH = /home/ronmi/play/forgejo-mcp/fj11
RUN_MODE = prod
[database]
DB_TYPE = sqlite3
HOST = 127.0.0.1:3306
NAME = forgejo
USER = forgejo
PASSWD =
SCHEMA =
SSL_MODE = disable
PATH = forgejo.db
LOG_SQL = false
[repository]
ROOT = forgejo-repositories
[server]
SSH_DOMAIN = 127.0.0.1
DOMAIN = 127.0.0.1
HTTP_PORT = 3000
ROOT_URL = http://127.0.0.1:3000/
APP_DATA_PATH = data
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = 6EF7CRA8bpMQ0mVXbJt4JfG4PPJUDvdl6U5sQ0Pklks
OFFLINE_MODE = true
[lfs]
PATH = lfs
[mailer]
ENABLED = false
[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = true
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.localhost
[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true
[cron.update_checker]
ENABLED = false
[session]
PROVIDER = file
[log]
MODE = console
LEVEL = info
ROOT_PATH = log
[repository.pull-request]
DEFAULT_MERGE_STYLE = merge
[repository.signing]
DEFAULT_TRUST_MODEL = committer
[security]
INSTALL_LOCK = true
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3NTQyMTIzOTl9.4L70ToHVa7DEtGItRzj0L75VNVJ-GkumOJWOQmgA7pI
PASSWORD_HASH_ALGO = pbkdf2_hi
[oauth2]
JWT_SECRET = jt_ZEI5js6wnRWw21KH4WFti-WuRiVg4ukzQ-VX7EsU
```
--------------------------------------------------------------------------------
/cmd/stdio.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package cmd
import (
"context"
"fmt"
"os"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// stdioCmd represents the stdio command
var stdioCmd = &cobra.Command{
Use: "stdio",
Short: "Run MCP server in stdio mode",
Long: `Run the Forgejo MCP server using stdio transport for communication
with MCP clients through JSON-RPC over standard input/output.
This transport mode is ideal for:
- Local integrations and development
- Direct process communication
- Applications that can spawn child processes
Example:
forgejo-mcp stdio --server https://git.example.com --token your_token`,
Run: func(cmd *cobra.Command, args []string) {
base := viper.GetString("server")
token := viper.GetString("token")
if base == "" || token == "" {
cmd.Help()
os.Exit(1)
}
cl, err := tools.NewClient(base, token, "", nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating SDK client: %v\n", err)
os.Exit(1)
}
server := createServer(cl)
err = server.Run(context.TODO(), mcp.NewStdioTransport())
fmt.Fprintf(os.Stderr, "Server exited with error: %v\n", err)
if err != nil {
os.Exit(1)
}
},
}
func init() {
rootCmd.AddCommand(stdioCmd)
}
```
--------------------------------------------------------------------------------
/proposal.md:
--------------------------------------------------------------------------------
```markdown
# Forgejo MCP Server Requirements Specification
## Project Objective
Create an MCP (Model Context Protocol) server that enables Claude Code to manage repositories on Gitea/Forgejo servers.
### Core Requirements
1. **Communication Protocol**
- Support stdio mode for Claude Code integration
- Use official Go SDK (github.com/modelcontextprotocol/go-sdk)
2. **Feature Scope**
- See `features.md`
3. **Configuration Management**
- CLI arguments: `--server` (Forgejo server URL), `--token` (access token)
- Environment variables: `FORGEJOMCP_SERVER`, `FORGEJOMCP_TOKEN`
- Configuration priority: CLI args > Environment variables
### Technical Specifications
- **Programming Language**: Go
- **Main Dependencies**:
* Official MCP Go SDK (github.com/modelcontextprotocol/go-sdk)
* Forgejo SDK (codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2)
* CLI Framework (github.com/spf13/cobra)
* Configuration Management (github.com/spf13/viper)
- **Response Format**:
* Errors returned in plaintext format
* Normal responses returned in markdown format
## Development Principles
- Test-Driven Development (TDD)
- Agile development, starting with MVP
- Independent development and testing for each feature
- Following pair programming principles
## Documentation, Comments, and Messages
Considering this is an open source project, comments and messages (such as logging) must be in English.
Document files without language annotation should use English. Files with tw annotation (e.g., xxx.tw.md) should use Traditional Chinese.
```
--------------------------------------------------------------------------------
/types/test_helpers.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"strings"
"testing"
"time"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// assertContains checks if all required strings are present in output
func assertContains(t *testing.T, output string, required []string) {
t.Helper()
for _, req := range required {
if !strings.Contains(output, req) {
t.Errorf("Expected output to contain %q, but it didn't. Output: %s", req, output)
}
}
}
// testTime returns a consistent test time
func testTime() time.Time {
return time.Date(2024, 1, 15, 14, 30, 0, 0, time.UTC)
}
// testUser creates a test user with typical data
func testUser() *forgejo.User {
return &forgejo.User{
ID: 123,
UserName: "testuser",
FullName: "Test User",
Email: "[email protected]",
}
}
// testMilestone creates a test milestone with typical data
func testMilestone() *forgejo.Milestone {
deadline := testTime()
return &forgejo.Milestone{
ID: 1,
Title: "v1.0.0",
Description: "Major release",
State: "open",
OpenIssues: 5,
ClosedIssues: 10,
Deadline: &deadline,
}
}
// testLabel creates a test label with typical data
func testLabel() *forgejo.Label {
return &forgejo.Label{
ID: 1,
Name: "bug",
Color: "ff0000",
Description: "Something isn't working",
}
}
```
--------------------------------------------------------------------------------
/types/milestone_test.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"testing"
)
func TestMilestone_ToMarkdown(t *testing.T) {
tests := []struct {
name string
milestone *Milestone
required []string
}{
{
name: "complete milestone with all fields",
milestone: &Milestone{
Milestone: testMilestone(),
},
required: []string{"v1.0.0", "open", "2024-01-15", "10/15", "Major release"},
},
{
name: "nil milestone",
milestone: &Milestone{Milestone: nil},
required: []string{"Invalid milestone"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.milestone.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestMilestoneList_ToMarkdown(t *testing.T) {
tests := []struct {
name string
milestones MilestoneList
required []string
}{
{
name: "multiple milestones with complete information",
milestones: MilestoneList{
&Milestone{Milestone: testMilestone()},
&Milestone{
Milestone: testMilestone(),
},
},
required: []string{"1.", "v1.0.0", "open", "2024-01-15", "Progress: 10/15", "2.", "v1.0.0"},
},
{
name: "empty milestone list",
milestones: MilestoneList{},
required: []string{"No milestones found"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.milestones.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
```
--------------------------------------------------------------------------------
/types/label_test.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"testing"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
func TestLabel_ToMarkdown(t *testing.T) {
tests := []struct {
name string
label *Label
required []string
}{
{
name: "complete label with all fields",
label: &Label{
Label: testLabel(),
},
required: []string{"bug", "ff0000", "Something isn't working"},
},
{
name: "nil label",
label: &Label{Label: nil},
required: []string{"Invalid label"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.label.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestLabelList_ToMarkdown(t *testing.T) {
tests := []struct {
name string
labels LabelList
required []string
}{
{
name: "multiple labels with complete information",
labels: LabelList{
&Label{Label: testLabel()},
&Label{
Label: &forgejo.Label{
Name: "enhancement",
Color: "a2eeef",
Description: "New feature or request",
},
},
},
required: []string{"bug", "ff0000", "Something isn't working", "enhancement", "a2eeef", "New feature or request"},
},
{
name: "empty label list",
labels: LabelList{},
required: []string{"No labels found"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.labels.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
```
--------------------------------------------------------------------------------
/types/labels.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// Label represents a label response with embedded SDK label
// Used by endpoints:
// - GET /repos/{owner}/{repo}/labels (list)
// - POST /repos/{owner}/{repo}/labels (create)
// - PATCH /repos/{owner}/{repo}/labels/{id} (edit)
// - POST /repos/{owner}/{repo}/issues/{index}/labels (add to issue)
// - PUT /repos/{owner}/{repo}/issues/{index}/labels (replace issue labels)
// - DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id} (remove from issue)
type Label struct {
*forgejo.Label
}
// ToMarkdown renders a label as a colored badge with name and description
// Example: **bug** `#ff0000` - Something isn't working
func (l *Label) ToMarkdown() string {
if l.Label == nil {
return "*Invalid label*"
}
markdown := "**" + l.Name + "**"
if l.Color != "" {
markdown += " `#" + l.Color + "`"
}
if l.Description != "" {
markdown += " - " + l.Description
}
return markdown
}
// LabelList represents a list of labels response
// Used by endpoints:
// - GET /repos/{owner}/{repo}/labels
// - POST /repos/{owner}/{repo}/issues/{index}/labels
// - PUT /repos/{owner}/{repo}/issues/{index}/labels
type LabelList []*Label
// ToMarkdown renders labels as a bullet list of colored badges
// Example:
// - **bug** `#ff0000` - Something isn't working
// - **enhancement** `#a2eeef` - New feature or request
func (ll LabelList) ToMarkdown() string {
if len(ll) == 0 {
return "*No labels found*"
}
markdown := ""
for _, label := range ll {
markdown += "- " + label.ToMarkdown() + "\n"
}
return markdown
}
```
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
```
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<style>
.bg-line { stroke: #E0E0E0; stroke-width: 1.5; }
.bg-node { fill: #E0E0E0; }
.active-line {
stroke: #4FC3F7; /* Light Blue */
stroke-width: 2.5;
stroke-linecap: round;
}
.active-node { fill: #4FC3F7; }
.spark {
fill: #FFD600; /* Amber Yellow */
stroke: #FFB300;
stroke-width: 0.5;
}
.f-foreground {
fill: #37474F; /* Dark Slate Grey */
stroke: #FFFFFF; /* White outline for contrast */
stroke-width: 2.5;
stroke-linejoin: round;
}
</style>
<rect width="100" height="100" fill="#FFFFFF"/>
<!-- Background Neural Network -->
<g id="neural-network">
<!-- Inactive paths and nodes -->
<path class="bg-line" d="M15 25 L30 40 L10 60 M30 40 L50 50 L45 80 M50 50 L75 75 M50 50 L80 55 L90 30"/>
<circle class="bg-node" cx="15" cy="25" r="4"/>
<circle class="bg-node" cx="10" cy="60" r="4"/>
<circle class="bg-node" cx="45" cy="80" r="4"/>
<circle class="bg-node" cx="75" cy="75" r="4"/>
<circle class="bg-node" cx="90" cy="30" r="4"/>
<!-- Active Path -->
<path class="active-line" d="M20 85 L40 65 L65 50 L85 20"/>
<circle class="bg-node" cx="20" cy="85" r="4"/>
<circle class="active-node" cx="40" cy="65" r="5"/>
<circle class="active-node" cx="65" cy="50" r="5"/>
<circle class="active-node" cx="85" cy="20" r="5"/>
<!-- Spark on an active node -->
<g transform="translate(85, 20) scale(0.8)">
<path class="spark" d="M0 -10 L2 -2 L10 0 L2 2 L0 10 L-2 2 L-10 0 L-2 -2 Z" />
</g>
</g>
<!-- Foreground F (New "Protocol" Style) -->
<!-- This single path contains three disconnected shapes for the F -->
<path class="f-foreground" d="M25 15 H 40 V 85 H 25 Z M48 15 H 70 V 30 H 48 Z M48 40 H 65 V 55 H 48 Z" />
</svg>
```
--------------------------------------------------------------------------------
/types/releases.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// Release represents a release response with embedded SDK release
// Used by endpoints:
// - GET /repos/{owner}/{repo}/releases (list)
// - POST /repos/{owner}/{repo}/releases (create)
// - PATCH /repos/{owner}/{repo}/releases/{id} (edit)
type Release struct {
*forgejo.Release
}
// ToMarkdown renders release with tag, name, draft/prerelease status and description
// Example: **v1.0.0** - Major Release `PRERELEASE` (2024-01-15)
// This release includes new authentication system and bug fixes...
func (r *Release) ToMarkdown() string {
if r.Release == nil {
return "*Invalid release*"
}
markdown := "**" + r.TagName + "**"
if r.Title != "" {
markdown += " - " + r.Title
}
if r.IsDraft {
markdown += " `DRAFT`"
}
if r.IsPrerelease {
markdown += " `PRERELEASE`"
}
if !r.CreatedAt.IsZero() {
markdown += " (" + r.CreatedAt.Format("2006-01-02") + ")"
}
if r.Note != "" {
markdown += "\n" + r.Note
}
return markdown
}
// ReleaseList represents a list of releases response
// Used by endpoints:
// - GET /repos/{owner}/{repo}/releases
type ReleaseList []*Release
// ToMarkdown renders releases as a numbered list with details
// Example:
// 1. **v1.0.0** - Major Release `PRERELEASE` (2024-01-15)
// This release includes new authentication system...
// 2. **v0.9.0** - Beta Release (2024-01-01)
// Initial beta version with core features
func (rl ReleaseList) ToMarkdown() string {
if len(rl) == 0 {
return "*No releases found*"
}
markdown := ""
for i, release := range rl {
markdown += fmt.Sprintf("%d. %s\n", i+1, release.ToMarkdown())
}
return markdown
}
```
--------------------------------------------------------------------------------
/types/pullrequests.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// PullRequest represents a pull request response with embedded SDK pull request
// Used by endpoints:
// - GET /repos/{owner}/{repo}/pulls (list)
// - GET /repos/{owner}/{repo}/pulls/{index} (get)
type PullRequest struct {
*forgejo.PullRequest
}
// ToMarkdown renders pull request with title, state, author and branch info
// Example: **#42 Add user authentication** (open)
// Author: johndoe
// Branch: feature/auth → main
//
// This PR implements OAuth2 authentication...
func (pr *PullRequest) ToMarkdown() string {
if pr.PullRequest == nil {
return "*Invalid pull request*"
}
markdown := fmt.Sprintf("**#%d %s** (%s)\n", pr.Index, pr.Title, pr.State)
if pr.Poster != nil {
markdown += "Author: " + pr.Poster.UserName + "\n"
}
if pr.Head != nil && pr.Base != nil {
markdown += fmt.Sprintf("Branch: %s → %s\n", pr.Head.Name, pr.Base.Name)
}
if pr.Body != "" {
markdown += "\n" + pr.Body
}
return markdown
}
// PullRequestList represents a list of pull requests response
// Used by endpoints:
// - GET /repos/{owner}/{repo}/pulls
type PullRequestList []*PullRequest
// ToMarkdown renders pull requests as a numbered list with basic info
// Example:
// 1. **#42 Add user authentication** (open)
// Author: johndoe
// Branch: feature/auth → main
//
// This PR implements OAuth2 authentication...
// 2. **#41 Fix database connection** (merged)
// Author: alice
// Branch: bugfix/db → main
func (prl PullRequestList) ToMarkdown() string {
if len(prl) == 0 {
return "*No pull requests found*"
}
markdown := ""
for i, pr := range prl {
markdown += fmt.Sprintf("%d. %s\n", i+1, pr.ToMarkdown())
}
return markdown
}
```
--------------------------------------------------------------------------------
/tools/client_issue_attachments.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package tools
import (
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// MyEditAttachmentOptions extends the SDK version with missing fields.
type MyEditAttachmentOptions struct {
Name string `json:"name,omitempty"`
DownloadURL string `json:"browser_download_url,omitempty"`
}
// MyListIssueAttachments lists all attachments of an issue.
// GET /repos/{owner}/{repo}/issues/{index}/assets
func (c *Client) MyListIssueAttachments(owner, repo string, index int64) ([]*forgejo.Attachment, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", owner, repo, index)
var result []*forgejo.Attachment
err := c.sendSimpleRequest("GET", endpoint, nil, &result)
if err != nil {
return nil, err
}
return result, nil
}
// MyDeleteIssueAttachment deletes an attachment from an issue.
// DELETE /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}
func (c *Client) MyDeleteIssueAttachment(owner, repo string, index, attachmentID int64) error {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", owner, repo, index, attachmentID)
// DELETE returns 204 No Content on success, so we can use nil as response target
var result interface{}
err := c.sendSimpleRequest("DELETE", endpoint, nil, &result)
if err != nil {
return err
}
return nil
}
// MyEditIssueAttachment edits an attachment of an issue.
// PATCH /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}
func (c *Client) MyEditIssueAttachment(owner, repo string, index, attachmentID int64, options MyEditAttachmentOptions) (*forgejo.Attachment, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", owner, repo, index, attachmentID)
var result forgejo.Attachment
err := c.sendSimpleRequest("PATCH", endpoint, options, &result)
if err != nil {
return nil, err
}
return &result, nil
}
```
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package cmd
import (
"os"
"github.com/raohwork/forgejo-mcp/types"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Version: types.VERSION,
Use: "forgejo-mcp",
Short: "Forgejo MCP Server for Model Context Protocol clients",
Long: `Forgejo MCP Server provides Model Context Protocol integration
for managing Gitea/Forgejo repositories through MCP-compatible clients.
Supported operations:
- Issues (create, edit, comment, close, manage attachments, dependencies/blocking)
- Labels (list, create, edit, delete)
- Milestones (list, create, edit, delete)
- Releases (list, create, edit, delete, manage attachments)
- Pull requests (list, view)
- Repository search and listing
- Wiki pages (create, edit, delete, list)
- Forgejo Actions tasks (list)
Available transport modes:
- stdio: Standard input/output (best for local integration)
- http: HTTP server with SSE and Streamable HTTP support (best for web apps and remote access)
Configure your Forgejo instance:
forgejo-mcp [mode] --server https://git.example.com --token your_token
Environment variables (alternative to command line arguments):
FORGEJOMCP_SERVER - Forgejo server URL
FORGEJOMCP_TOKEN - Access token`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
f := rootCmd.PersistentFlags()
f.String("server", "", "Forgejo server URL (env: FORGEJOMCP_SERVER)")
f.String("token", "", "Forgejo access token (env: FORGEJOMCP_TOKEN)")
viper.BindPFlags(f)
viper.SetEnvPrefix("FORGEJOMCP")
viper.AutomaticEnv()
}
```
--------------------------------------------------------------------------------
/types/attachments.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// Attachment represents an attachment response with embedded SDK attachment
// Used by endpoints:
// - GET /repos/{owner}/{repo}/releases/{id}/assets (list release attachments)
// - POST /repos/{owner}/{repo}/releases/{id}/assets (create release attachment)
// - PATCH /repos/{owner}/{repo}/releases/assets/{id} (edit release attachment)
// - GET /repos/{owner}/{repo}/issues/{index}/attachments (list issue attachments)
// - POST /repos/{owner}/{repo}/issues/{index}/attachments (create issue attachment)
// - PATCH /repos/{owner}/{repo}/issues/{index}/attachments/{attachment_id} (edit issue attachment)
type Attachment struct {
*forgejo.Attachment
}
// ToMarkdown renders attachment with name, size and download link
// Example: **document.pdf** (1024 bytes) [Download](https://git.example.com/attachments/123)
func (a *Attachment) ToMarkdown() string {
if a.Attachment == nil {
return "*Invalid attachment*"
}
markdown := "**" + a.Name + "**"
if a.Size > 0 {
markdown += fmt.Sprintf(" (%d bytes)", a.Size)
}
if a.DownloadURL != "" {
markdown += " [Download](" + a.DownloadURL + ")"
}
return markdown
}
// AttachmentList represents a list of attachments response
// Used by endpoints:
// - GET /repos/{owner}/{repo}/releases/{id}/assets
// - GET /repos/{owner}/{repo}/issues/{index}/attachments
type AttachmentList []*Attachment
// ToMarkdown renders attachments as a bullet list with download info
// Example:
// - **document.pdf** (1024 bytes) [Download](https://git.example.com/attachments/123)
// - **screenshot.png** (2048 bytes) [Download](https://git.example.com/attachments/124)
func (al AttachmentList) ToMarkdown() string {
if len(al) == 0 {
return "*No attachments found*"
}
markdown := ""
for _, attachment := range al {
markdown += "- " + attachment.ToMarkdown() + "\n"
}
return markdown
}
```
--------------------------------------------------------------------------------
/types/repositories.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// Repository represents a repository response with embedded SDK repository
// Used by endpoints:
// - GET /repos/search
// - GET /user/repos
// - GET /orgs/{org}/repos
type Repository struct {
*forgejo.Repository
}
// ToMarkdown renders repository with name, description, stats and key info
// Example: **owner/repo-name** `PRIVATE` `FORK`
// A sample repository for testing purposes
// Stars: 42 | Forks: 7 | Issues: 3 | PRs: 1
// [View Repository](https://git.example.com/owner/repo-name)
func (r *Repository) ToMarkdown() string {
if r.Repository == nil {
return "*Invalid repository*"
}
markdown := "**" + r.FullName + "**"
if r.Private {
markdown += " `PRIVATE`"
}
if r.Fork {
markdown += " `FORK`"
}
if r.Template {
markdown += " `TEMPLATE`"
}
markdown += "\n"
if r.Description != "" {
markdown += r.Description + "\n"
}
markdown += fmt.Sprintf("Stars: %d | Forks: %d | Issues: %d | PRs: %d\n", r.Stars, r.Forks, r.OpenIssues, r.OpenPulls)
if r.HTMLURL != "" {
markdown += "[View Repository](" + r.HTMLURL + ")"
}
return markdown
}
// RepositoryList represents a list of repositories response
// Used by endpoints:
// - GET /repos/search
// - GET /user/repos
// - GET /orgs/{org}/repos
type RepositoryList []*Repository
// ToMarkdown renders repositories as a numbered list with basic stats
// Example:
// 1. **owner/repo-name** `PRIVATE` `FORK`
// A sample repository for testing purposes
// Stars: 42 | Forks: 7 | Issues: 3 | PRs: 1
// [View Repository](https://git.example.com/owner/repo-name)
// 2. **owner/another-repo**
// Another repository
// Stars: 15 | Forks: 2 | Issues: 0 | PRs: 0
func (rl RepositoryList) ToMarkdown() string {
if len(rl) == 0 {
return "*No repositories found*"
}
markdown := ""
for i, repo := range rl {
markdown += fmt.Sprintf("%d. %s\n", i+1, repo.ToMarkdown())
}
return markdown
}
```
--------------------------------------------------------------------------------
/types/dependencies.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// MyIssueMeta represents basic issue information for dependency operations.
// This type is not available in the Forgejo SDK.
type MyIssueMeta struct {
Index int64 `json:"index"`
Owner string `json:"owner,omitempty"`
Name string `json:"repo,omitempty"`
}
// IssueDependencyList represents a list of issues that block the current issue.
// According to Forgejo API definition, these are issues that must be closed
// before the current issue can be closed.
// Used by list_issue_dependencies endpoint.
type IssueDependencyList []*forgejo.Issue
// ToMarkdown renders issue dependencies with essential information for quick scanning
// Shows: #Index **Title** (state)
// Example per issue:
// #123 **Fix authentication bug** (open)
// #45 **Update user model** (closed)
func (idl IssueDependencyList) ToMarkdown() string {
if len(idl) == 0 {
return "*No issue dependencies found*"
}
markdown := ""
for _, issue := range idl {
if issue == nil {
continue
}
markdown += fmt.Sprintf("#%d **%s** (%s)\n", issue.Index, issue.Title, issue.State)
}
return markdown
}
// IssueBlockingList represents a list of issues that are blocked by the current issue.
// According to Forgejo API definition, these are issues that cannot be closed
// until the current issue is closed.
// Used by list_issue_blocking endpoint.
type IssueBlockingList []*forgejo.Issue
// ToMarkdown renders issue blocking list with essential information for quick scanning
// Shows: #Index **Title** (state)
// Example per issue:
// #123 **Fix authentication bug** (open)
// #45 **Update user model** (closed)
func (ibl IssueBlockingList) ToMarkdown() string {
if len(ibl) == 0 {
return "*This issue is not blocking any other issues*"
}
markdown := ""
for _, issue := range ibl {
if issue == nil {
continue
}
markdown += fmt.Sprintf("#%d **%s** (%s)\n", issue.Index, issue.Title, issue.State)
}
return markdown
}
```
--------------------------------------------------------------------------------
/types/wiki_test.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"encoding/base64"
"testing"
"time"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
func TestWikiPage_ToMarkdown(t *testing.T) {
modified := testTime()
tests := []struct {
name string
wiki *WikiPage
required []string
}{
{
name: "complete wiki page with all fields",
wiki: &WikiPage{
MyWikiPage: &MyWikiPage{
Title: "Getting Started",
ContentBase64: base64.StdEncoding.EncodeToString([]byte("Welcome to our project wiki. This guide will help you get started with the project.")),
LastCommit: &MyWikiCommit{
Author: &forgejo.CommitUser{
Date: modified.Format(time.RFC3339),
},
},
},
},
required: []string{"# Getting Started", "Last modified: 2024-01-15 14:30", "Welcome to our project wiki"},
},
{
name: "wiki page without content",
wiki: &WikiPage{
MyWikiPage: &MyWikiPage{
Title: "Empty Page",
},
},
required: []string{"# Empty Page"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.wiki.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestWikiPageList_ToMarkdown(t *testing.T) {
modified := testTime()
tests := []struct {
name string
wikis WikiPageList
required []string
}{
{
name: "multiple wiki pages with complete information",
wikis: WikiPageList{
&MyWikiPageMetaData{
Title: "Getting Started",
LastCommit: &MyWikiCommit{
Author: &forgejo.CommitUser{
Date: modified.Format(time.RFC3339),
},
},
},
&MyWikiPageMetaData{
Title: "API Documentation",
LastCommit: &MyWikiCommit{
Author: &forgejo.CommitUser{
Date: modified.Format(time.RFC3339),
},
},
},
&MyWikiPageMetaData{
Title: "Contributing Guide",
},
},
required: []string{"## Wiki Pages", "Getting Started", "API Documentation", "Contributing Guide", "2024-01-15"},
},
{
name: "empty wiki page list",
wikis: WikiPageList{},
required: []string{"*No wiki pages found*"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.wikis.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
```
--------------------------------------------------------------------------------
/types/dependencies_test.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"testing"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
func TestIssueDependencyList_ToMarkdown(t *testing.T) {
tests := []struct {
name string
dependencies IssueDependencyList
required []string
}{
{
name: "multiple issue dependencies",
dependencies: IssueDependencyList{
&forgejo.Issue{
Index: 123,
Title: "Fix authentication bug",
State: "open",
},
&forgejo.Issue{
Index: 45,
Title: "Update user model",
State: "closed",
},
},
required: []string{"#123", "**Fix authentication bug**", "(open)", "#45", "**Update user model**", "(closed)"},
},
{
name: "empty dependency list",
dependencies: IssueDependencyList{},
required: []string{"*No issue dependencies found*"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.dependencies.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestIssueBlockingList_ToMarkdown(t *testing.T) {
tests := []struct {
name string
blocking IssueBlockingList
required []string
}{
{
name: "multiple issue blocking",
blocking: IssueBlockingList{
&forgejo.Issue{
Index: 234,
Title: "Update user interface",
State: "open",
},
&forgejo.Issue{
Index: 567,
Title: "Add new feature",
State: "closed",
},
},
required: []string{"#234", "**Update user interface**", "(open)", "#567", "**Add new feature**", "(closed)"},
},
{
name: "empty blocking list",
blocking: IssueBlockingList{},
required: []string{"*This issue is not blocking any other issues*"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.blocking.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestEmptyResponse_ToMarkdown(t *testing.T) {
tests := []struct {
name string
response EmptyResponse
required []string
}{
{
name: "empty response",
response: EmptyResponse{},
required: []string{"Operation completed successfully"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.response.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
```
--------------------------------------------------------------------------------
/tools/client_wiki.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package tools
import (
"fmt"
"github.com/raohwork/forgejo-mcp/types"
)
// MyListWikiPages lists all wiki pages in a repository.
// GET /repos/{owner}/{repo}/wiki/pages
func (c *Client) MyListWikiPages(owner, repo string) ([]*types.MyWikiPageMetaData, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/pages", owner, repo)
var result []*types.MyWikiPageMetaData
err := c.sendSimpleRequest("GET", endpoint, nil, &result)
if err != nil {
return nil, err
}
return result, nil
}
// MyGetWikiPage gets a single wiki page by name.
// GET /repos/{owner}/{repo}/wiki/page/{pageName}
func (c *Client) MyGetWikiPage(owner, repo, pageName string) (*types.MyWikiPage, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
var result types.MyWikiPage
err := c.sendSimpleRequest("GET", endpoint, nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// MyCreateWikiPage creates a new wiki page.
// POST /repos/{owner}/{repo}/wiki/new
func (c *Client) MyCreateWikiPage(owner, repo string, options types.MyCreateWikiPageOptions) (*types.MyWikiPage, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", owner, repo)
var result types.MyWikiPage
err := c.sendSimpleRequest("POST", endpoint, options, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// MyDeleteWikiPage deletes a wiki page.
// DELETE /repos/{owner}/{repo}/wiki/page/{pageName}
func (c *Client) MyDeleteWikiPage(owner, repo, pageName string) error {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
// DELETE returns 204 No Content on success
var result interface{}
err := c.sendSimpleRequest("DELETE", endpoint, nil, &result)
if err != nil {
return err
}
return nil
}
// MyEditWikiPage edits an existing wiki page.
// PATCH /repos/{owner}/{repo}/wiki/page/{pageName}
func (c *Client) MyEditWikiPage(owner, repo, pageName string, options types.MyCreateWikiPageOptions) (*types.MyWikiPage, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
var result types.MyWikiPage
err := c.sendSimpleRequest("PATCH", endpoint, options, &result)
if err != nil {
return nil, err
}
return &result, nil
}
```
--------------------------------------------------------------------------------
/types/milestones.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// Milestone represents a milestone response with embedded SDK milestone
// Used by endpoints:
// - GET /repos/{owner}/{repo}/milestones (list)
// - POST /repos/{owner}/{repo}/milestones (create)
// - PATCH /repos/{owner}/{repo}/milestones/{id} (edit)
type Milestone struct {
*forgejo.Milestone
}
// ToMarkdown renders milestone with title, state, due date and progress
// Example: **v1.0.0** (open) - Due: 2024-12-31 - Progress: 5/10
// Fix critical bugs before release
func (m *Milestone) ToMarkdown() string {
if m.Milestone == nil {
return "*Invalid milestone*"
}
markdown := fmt.Sprintf("**%s** #%d", m.Title, m.ID)
if m.State != "" {
markdown += " (" + string(m.State) + ")"
}
if m.Deadline != nil {
markdown += " - Due: " + m.Deadline.Format("2006-01-02")
}
if m.ClosedIssues > 0 || m.OpenIssues > 0 {
total := m.ClosedIssues + m.OpenIssues
markdown += " - Progress: " + fmt.Sprintf("%d/%d", m.ClosedIssues, total)
}
if m.Description != "" {
markdown += "\n" + m.Description
}
return markdown
}
// MilestoneList represents a list of milestones response
// Used by endpoints:
// - GET /repos/{owner}/{repo}/milestones
type MilestoneList []*Milestone
// ToMarkdown renders milestones as a numbered list with essential details only
// Description is omitted to reduce memory usage for AI assistants
// Example:
// 1. **v1.0.0** (open) - Due: 2024-12-31 - Progress: 5/10
// 2. **v0.9.0** (closed) - Progress: 10/10
func (ml MilestoneList) ToMarkdown() string {
if len(ml) == 0 {
return "*No milestones found*"
}
markdown := ""
for i, milestone := range ml {
if milestone.Milestone == nil {
markdown += fmt.Sprintf("%d. *Invalid milestone*\n", i+1)
continue
}
// Format: **Title** (state) - Due: date - Progress: closed/total
line := fmt.Sprintf("%d. **%s** #%d", i+1, milestone.Title, milestone.ID)
if milestone.State != "" {
line += " (" + string(milestone.State) + ")"
}
if milestone.Deadline != nil {
line += " - Due: " + milestone.Deadline.Format("2006-01-02")
}
if milestone.ClosedIssues > 0 || milestone.OpenIssues > 0 {
total := milestone.ClosedIssues + milestone.OpenIssues
line += " - Progress: " + fmt.Sprintf("%d/%d", milestone.ClosedIssues, total)
}
markdown += line + "\n"
}
return markdown
}
```
--------------------------------------------------------------------------------
/types/actions.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"fmt"
"time"
)
// ActionTaskList represents a list of action tasks response
// Used by endpoints:
// - GET /repos/{owner}/{repo}/actions/tasks
type ActionTaskList struct {
*MyActionTaskResponse
}
// ToMarkdown renders action tasks as a numbered list with status
// Example:
// 1. **Build and Test** `success`
// Run ID: 123 | Job ID: 456
// Started: 2024-01-15 14:30 | Duration: 2m15s
// Steps:
// - Setup Go `success`
// - Run Tests `success`
//
// 2. **Deploy** `running`
// Run ID: 124 | Job ID: 457
// Started: 2024-01-15 14:35
func (atl ActionTaskList) ToMarkdown() string {
if atl.MyActionTaskResponse == nil || len(atl.WorkflowRuns) == 0 {
return "*No action tasks found*"
}
markdown := ""
for i, task := range atl.WorkflowRuns {
markdown += fmt.Sprintf("%d. %s\n", i+1, task.ToMarkdown())
}
return markdown
}
// MyActionTask represents a Forgejo Actions task.
type MyActionTask struct {
ID int64 `json:"id"`
Name string `json:"name"`
DisplayTitle string `json:"display_title"` // title of last commit
Status string `json:"status"`
Event string `json:"event"` //push, pull_request, etc.
WorkflowID string `json:"workflow_id"` // filename of yaml
HeadBranch string `json:"head_branch"`
HeadSHA string `json:"head_sha"`
RunNumber int64 `json:"run_number"` // run#N
URL string `json:"url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
RunStartedAt time.Time `json:"run_started_at"`
}
// ToMarkdown renders action task with name, status, execution info and timing
func (at *MyActionTask) ToMarkdown() string {
markdown := fmt.Sprintf("**%s** `%s` - Run #%d", at.DisplayTitle, at.Status, at.RunNumber)
// Add timing information for statistical analysis
if !at.CreatedAt.IsZero() {
markdown += fmt.Sprintf(" | Created: %s", at.CreatedAt.Format("2006-01-02 15:04"))
}
// Add duration if both start and update times are available
if !at.RunStartedAt.IsZero() && !at.UpdatedAt.IsZero() {
duration := at.UpdatedAt.Sub(at.RunStartedAt)
markdown += fmt.Sprintf(" | Duration: %s", duration.String())
}
return markdown
}
// MyActionTaskResponse represents the response for listing action tasks.
type MyActionTaskResponse struct {
TotalCount int64 `json:"total_count"`
WorkflowRuns []*MyActionTask `json:"workflow_runs"`
}
```
--------------------------------------------------------------------------------
/cmd/http.go:
--------------------------------------------------------------------------------
```go
/*
Copyright © 2025 Ronmi Ren <[email protected]
*/
package cmd
import (
"fmt"
"net/http"
"os"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// httpCmd represents the http command
var httpCmd = &cobra.Command{
Use: "http",
Short: "Run MCP server in SSE mode",
Long: `Run the Forgejo MCP server with an HTTP interface.
This command starts a web server that allows MCP clients to connect over HTTP.
It supports both Server-Sent Events (SSE) on the /sse endpoint and a standard
request/response model on the / endpoint.
The server can operate in two modes:
- Single-user mode: When a --token is provided at startup, all operations
are performed using this single token. This is suitable for personal use
or dedicated services where one identity is sufficient.
- Multi-user mode: If no --token is provided, the server requires clients
to authenticate by providing their own token in the 'Authorization'
header of each request. This allows the server to act as a gateway for
multiple users.
This HTTP mode is ideal for:
- Web-based clients and services.
- Remote access to the Forgejo instance through MCP.
- Environments where a central MCP gateway is needed.
Example:
forgejo-mcp http --address :8080 --server https://git.example.com --token your_token`,
Run: func(cmd *cobra.Command, args []string) {
base := viper.GetString("server")
token := viper.GetString("token")
addr := viper.GetString("address")
if addr == "" {
addr = ":8080"
}
if base == "" {
cmd.Help()
os.Exit(1)
}
singleMode := token != ""
var cl *tools.Client
if singleMode {
c, err := tools.NewClient(base, token, "", nil)
if err != nil {
fmt.Printf("Error creating SDK client: %v\n", err)
os.Exit(1)
}
cl = c
} else {
cl, _ = tools.NewClient(base, "", "9", nil)
}
getServer := func(q *http.Request) *mcp.Server {
if singleMode {
return createServer(cl)
}
mycl := cl
myToken := q.Header.Get("Authorization")
if myToken != "" {
if strings.HasPrefix(myToken, "Bearer ") {
myToken = myToken[7:]
}
c, err := tools.NewClient(base, myToken, "", nil)
if err == nil {
mycl = c
}
}
return createServer(mycl)
}
mux := http.NewServeMux()
mux.Handle("/sse", mcp.NewSSEHandler(getServer))
mux.Handle("/", mcp.NewStreamableHTTPHandler(getServer, nil))
mode := "single"
if !singleMode {
mode = "multiuser"
}
fmt.Printf("Starting %s mode MCP server on %s\n", mode, addr)
err := http.ListenAndServe(addr, mux)
if err != nil {
fmt.Printf("Server exited with error: %v\n", err)
os.Exit(1)
}
},
}
func init() {
rootCmd.AddCommand(httpCmd)
f := httpCmd.Flags()
f.String("address", ":8080", "Address to listen on for incoming connections")
viper.BindPFlags(f)
}
```
--------------------------------------------------------------------------------
/tools/pullreq/view.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package pullreq
import (
"context"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// GetPullRequestParams defines the parameters for the get_pull_request tool.
// It specifies the pull request to retrieve by its owner, repository, and index.
type GetPullRequestParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the pull request number.
Index int `json:"index"`
}
// GetPullRequestImpl implements the read-only MCP tool for fetching a single pull request.
// This is a safe, idempotent operation that uses the Forgejo SDK to retrieve
// detailed information about a specific pull request.
type GetPullRequestImpl struct {
Client *tools.Client
}
// Definition describes the `get_pull_request` tool. It requires `owner`, `repo`,
// and the pull request `index`. It is marked as a safe, read-only operation.
func (GetPullRequestImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "get_pull_request",
Title: "Get Pull Request",
Description: "Get detailed information about a specific pull request including diff, commits, and review status.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Pull request index number",
},
},
Required: []string{"owner", "repo", "index"},
},
}
}
// Handler implements the logic for fetching a pull request. It calls the Forgejo
// SDK's `GetPullRequest` function and formats the result into a detailed markdown
// view. It will return an error if the pull request is not found.
func (impl GetPullRequestImpl) Handler() mcp.ToolHandlerFor[GetPullRequestParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args GetPullRequestParams) (*mcp.CallToolResult, any, error) {
p := args
// Call SDK
pr, _, err := impl.Client.GetPullRequest(p.Owner, p.Repo, int64(p.Index))
if err != nil {
return nil, nil, fmt.Errorf("failed to get pull request: %w", err)
}
// Convert to our type and format
prWrapper := &types.PullRequest{PullRequest: pr}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: prWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/types/action_test.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"testing"
"time"
)
func TestMyActionTask_ToMarkdown(t *testing.T) {
createdTime := testTime()
startedTime := createdTime.Add(1 * time.Minute)
updatedTime := startedTime.Add(5 * time.Minute)
tests := []struct {
name string
task *MyActionTask
required []string
}{
{
name: "complete action task with all fields",
task: &MyActionTask{
DisplayTitle: "Add new feature",
Status: "success",
RunNumber: 123,
WorkflowID: "ci.yml",
HeadBranch: "main",
Event: "push",
CreatedAt: createdTime,
RunStartedAt: startedTime,
UpdatedAt: updatedTime,
},
required: []string{"Add new feature", "success", "Run #123", "Created: 2024-01-15 14:30", "Duration: 5m0s"},
},
{
name: "failed action task with minimal timing",
task: &MyActionTask{
DisplayTitle: "Fix bug in authentication",
Status: "failure",
RunNumber: 456,
WorkflowID: "test.yml",
HeadBranch: "feature/auth-fix",
Event: "pull_request",
CreatedAt: createdTime,
},
required: []string{"Fix bug in authentication", "failure", "Run #456", "Created: 2024-01-15 14:30"},
},
{
name: "running task without timing",
task: &MyActionTask{
DisplayTitle: "Deploy to staging",
Status: "running",
RunNumber: 789,
},
required: []string{"Deploy to staging", "running", "Run #789"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.task.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestActionTaskList_ToMarkdown(t *testing.T) {
tests := []struct {
name string
tasks ActionTaskList
required []string
}{
{
name: "multiple action tasks with complete information",
tasks: ActionTaskList{
MyActionTaskResponse: &MyActionTaskResponse{
TotalCount: 2,
WorkflowRuns: []*MyActionTask{
{
DisplayTitle: "Add new feature",
Status: "success",
RunNumber: 123,
WorkflowID: "ci.yml",
},
{
DisplayTitle: "Fix authentication bug",
Status: "failure",
RunNumber: 124,
WorkflowID: "test.yml",
},
},
},
},
required: []string{"1.", "Add new feature", "success", "Run #123", "2.", "Fix authentication bug", "failure", "Run #124"},
},
{
name: "empty action task list",
tasks: ActionTaskList{
MyActionTaskResponse: &MyActionTaskResponse{
TotalCount: 0,
WorkflowRuns: []*MyActionTask{},
},
},
required: []string{"No action tasks found"},
},
{
name: "nil response",
tasks: ActionTaskList{},
required: []string{"No action tasks found"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.tasks.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
```
--------------------------------------------------------------------------------
/tools/wiki/list.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package wiki
import (
"context"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListWikiPagesParams defines the parameters for the list_wiki_pages tool.
// It specifies the owner and repository name to list wiki pages from.
type ListWikiPagesParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
}
// ListWikiPagesImpl implements the read-only MCP tool for listing repository wiki pages.
// This operation is safe, idempotent, and does not modify any data. It fetches
// all available wiki pages for a specified repository. Note: This feature is not
// supported by the official Forgejo SDK and requires a custom HTTP implementation.
type ListWikiPagesImpl struct {
Client *tools.Client
}
// Definition describes the `list_wiki_pages` tool. It requires `owner` and `repo`
// as parameters and is marked as a safe, read-only operation.
func (ListWikiPagesImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_wiki_pages",
Title: "List Wiki Pages",
Description: "List all wiki pages in a repository. Returns page information including titles and metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
},
Required: []string{"owner", "repo"},
},
}
}
// Handler implements the logic for listing wiki pages. It performs a custom
// HTTP GET request to the `/repos/{owner}/{repo}/wiki/pages` endpoint and
// formats the resulting list of pages into a markdown table. Errors will occur
// if the repository is not found or authentication fails.
func (impl ListWikiPagesImpl) Handler() mcp.ToolHandlerFor[ListWikiPagesParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListWikiPagesParams) (*mcp.CallToolResult, any, error) {
p := args
// Call custom client method
pages, err := impl.Client.MyListWikiPages(p.Owner, p.Repo)
if err != nil {
return nil, nil, fmt.Errorf("failed to list wiki pages: %w", err)
}
// Convert to our types and format
pageList := types.WikiPageList(pages)
var content string
if len(pages) == 0 {
content = "No wiki pages found in this repository."
} else {
content = fmt.Sprintf("Found %d wiki pages\n\n%s",
len(pages), pageList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/types/issue_test.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"testing"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
func TestIssue_ToMarkdown(t *testing.T) {
deadline := testTime()
tests := []struct {
name string
issue *Issue
required []string
}{
{
name: "complete issue with all fields",
issue: &Issue{
Issue: &forgejo.Issue{
Index: 123,
Title: "Fix login bug",
Body: "The login page crashes when using special characters",
State: "open",
Poster: testUser(),
Assignees: []*forgejo.User{testUser()},
Labels: []*forgejo.Label{
{Name: "bug"},
{Name: "priority-high"},
},
Milestone: testMilestone(),
Deadline: &deadline,
},
},
required: []string{"#123", "Fix login bug", "open", "testuser", "[testuser]", "[bug priority-high]", "v1.0.0", "2024-01-15", "The login page crashes"},
},
{
name: "nil issue",
issue: &Issue{Issue: nil},
required: []string{"Invalid issue"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.issue.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestIssueList_ToMarkdown(t *testing.T) {
updated := testTime()
tests := []struct {
name string
list IssueList
required []string
}{
{
name: "complete issue list with necessary fields",
list: IssueList{
&forgejo.Issue{
Index: 123,
Title: "Fix login bug",
State: "open",
Assignees: []*forgejo.User{testUser()},
Labels: []*forgejo.Label{
{Name: "bug"},
{Name: "priority-high"},
},
Updated: updated,
Comments: 5,
},
&forgejo.Issue{
Index: 456,
Title: "Simple issue",
State: "closed",
Updated: updated,
Comments: 0,
},
},
required: []string{
"#123", "Fix login bug", "open", "testuser", "bug", "priority-high", "2024-01-15", "5",
"#456", "Simple issue", "closed", "0",
},
},
{
name: "empty issue list",
list: IssueList{},
required: []string{"No issues found"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.list.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestComment_ToMarkdown(t *testing.T) {
created := testTime()
tests := []struct {
name string
comment *Comment
required []string
}{
{
name: "complete comment with all fields",
comment: &Comment{
Comment: &forgejo.Comment{
Poster: testUser(),
Body: "I think the issue is in the authentication module",
Created: created,
},
},
required: []string{"testuser", "2024-01-15 14:30", "I think the issue is in the authentication module"},
},
{
name: "nil comment",
comment: &Comment{Comment: nil},
required: []string{"Invalid comment"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.comment.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
```
--------------------------------------------------------------------------------
/types/wiki.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"encoding/base64"
"time"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// MyWikiCommit represents wiki page commit/revision information.
type MyWikiCommit struct {
ID string `json:"sha"`
Author *forgejo.CommitUser `json:"author"`
Committer *forgejo.CommitUser `json:"commiter"` // Note: API has typo "commiter"
Message string `json:"message"`
}
// MyWikiPageMetaData represents wiki page meta information.
type MyWikiPageMetaData struct {
Title string `json:"title"`
HTMLURL string `json:"html_url"`
SubURL string `json:"sub_url"`
LastCommit *MyWikiCommit `json:"last_commit"`
}
// MyWikiPage represents a complete wiki page with content.
type MyWikiPage struct {
Title string `json:"title"`
HTMLURL string `json:"html_url"`
SubURL string `json:"sub_url"`
LastCommit *MyWikiCommit `json:"last_commit"`
ContentBase64 string `json:"content_base64"`
CommitCount int64 `json:"commit_count"`
Sidebar string `json:"sidebar"`
Footer string `json:"footer"`
}
// MyCreateWikiPageOptions represents options for creating a wiki page.
type MyCreateWikiPageOptions struct {
Title string `json:"title"`
ContentBase64 string `json:"content_base64"`
Message string `json:"message,omitempty"`
}
// WikiPage represents a wiki page response (custom implementation as SDK doesn't support)
// Used by endpoints:
// - GET /repos/{owner}/{repo}/wiki/page/{pageName}
// - POST /repos/{owner}/{repo}/wiki/new
// - PATCH /repos/{owner}/{repo}/wiki/page/{pageName}
type WikiPage struct {
*MyWikiPage
}
// ToMarkdown renders wiki page with title, last modified date and content
// Example: # Getting Started
// *Last modified: 2024-01-15 14:30*
//
// Welcome to our project wiki...
func (w *WikiPage) ToMarkdown() string {
markdown := "# " + w.Title + "\n"
if w.LastCommit != nil && w.LastCommit.Author != nil && w.LastCommit.Author.Date != "" {
if t, err := time.Parse(time.RFC3339, w.LastCommit.Author.Date); err == nil {
markdown += "*Last modified: " + t.Format("2006-01-02 15:04") + "*\n\n"
}
}
if w.ContentBase64 != "" {
if content, err := base64.StdEncoding.DecodeString(w.ContentBase64); err == nil {
markdown += string(content)
}
}
return markdown
}
// WikiPageList represents a list of wiki pages response
// Used by endpoints:
// - GET /repos/{owner}/{repo}/wiki/pages
type WikiPageList []*MyWikiPageMetaData
// ToMarkdown renders wiki pages as a table of contents with links
// Example: ## Wiki Pages
// - **Getting Started** (2024-01-15)
// - **API Documentation** (2024-01-10)
// - **Contributing Guide** (2024-01-05)
func (wl WikiPageList) ToMarkdown() string {
if len(wl) == 0 {
return "*No wiki pages found*"
}
markdown := "## Wiki Pages\n"
for _, page := range wl {
markdown += "- **" + page.Title + "**"
if page.LastCommit != nil && page.LastCommit.Author != nil && page.LastCommit.Author.Date != "" {
if t, err := time.Parse(time.RFC3339, page.LastCommit.Author.Date); err == nil {
markdown += " (" + t.Format("2006-01-02") + ")"
}
}
markdown += "\n"
}
return markdown
}
```
--------------------------------------------------------------------------------
/tools/action/list.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package action
import (
"context"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListActionTasksParams defines the parameters for the list_action_tasks tool.
// It includes basic pagination options for Forgejo Actions tasks.
type ListActionTasksParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Page is the page number for pagination.
Page int `json:"page,omitempty"`
// Limit is the number of tasks to return per page.
Limit int `json:"limit,omitempty"`
}
// ListActionTasksImpl implements the read-only MCP tool for listing Forgejo Actions tasks.
// This is a safe, idempotent operation. Note: This feature is not supported by the
// official Forgejo SDK and requires a custom HTTP implementation.
type ListActionTasksImpl struct {
Client *tools.Client
}
// Definition describes the `list_action_tasks` tool. It requires `owner` and `repo`
// and supports various optional parameters for filtering. It is marked as a safe,
// read-only operation.
func (ListActionTasksImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_action_tasks",
Title: "List Action Tasks",
Description: "List Forgejo Actions execution tasks in a repository with basic pagination support.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"page": {
Type: "integer",
Description: "Page number for pagination (optional, defaults to 1)",
Minimum: tools.Float64Ptr(1),
},
"limit": {
Type: "integer",
Description: "Number of tasks per page (optional, defaults to 20, max 50)",
Minimum: tools.Float64Ptr(1),
Maximum: tools.Float64Ptr(50),
},
},
Required: []string{"owner", "repo"},
},
}
}
// Handler implements the logic for listing action tasks. It performs a custom HTTP
// GET request to the `/repos/{owner}/{repo}/actions/tasks` endpoint and formats
// the results into a markdown table.
func (impl ListActionTasksImpl) Handler() mcp.ToolHandlerFor[ListActionTasksParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListActionTasksParams) (*mcp.CallToolResult, any, error) {
p := args
// Call custom client method
response, err := impl.Client.MyListActionTasks(p.Owner, p.Repo)
if err != nil {
return nil, nil, fmt.Errorf("failed to list action tasks: %w", err)
}
// Convert to our types and format
var content string
if response.TotalCount == 0 || len(response.WorkflowRuns) == 0 {
content = "No action tasks found in this repository."
} else {
// Convert response to our type
taskList := types.ActionTaskList{
MyActionTaskResponse: response,
}
content = fmt.Sprintf("Found %d action tasks\n\n%s",
response.TotalCount, taskList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/client_issue_dependencies.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package tools
import (
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/raohwork/forgejo-mcp/types"
)
// MyAddIssueDependency adds a dependency to an issue.
// Creates a relationship where the current issue (index) depends on another issue (dependency).
// This means the dependency issue must be closed before the current issue can be closed.
// POST /repos/{owner}/{repo}/issues/{index}/dependencies
func (c *Client) MyAddIssueDependency(owner, repo string, index int64, dependency types.MyIssueMeta) (*forgejo.Issue, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", owner, repo, index)
var result forgejo.Issue
err := c.sendSimpleRequest("POST", endpoint, dependency, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// MyListIssueDependencies lists all dependencies of an issue.
// Returns issues that must be closed before the current issue can be closed.
// GET /repos/{owner}/{repo}/issues/{index}/dependencies
func (c *Client) MyListIssueDependencies(owner, repo string, index int64) ([]*forgejo.Issue, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", owner, repo, index)
var result []*forgejo.Issue
err := c.sendSimpleRequest("GET", endpoint, nil, &result)
if err != nil {
return nil, err
}
return result, nil
}
// MyRemoveIssueDependency removes a dependency from an issue.
// Removes the relationship where the current issue depends on another issue.
// DELETE /repos/{owner}/{repo}/issues/{index}/dependencies
func (c *Client) MyRemoveIssueDependency(owner, repo string, index int64, dependency types.MyIssueMeta) (*forgejo.Issue, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", owner, repo, index)
var result forgejo.Issue
err := c.sendSimpleRequest("DELETE", endpoint, dependency, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// MyListIssueBlocking lists all issues blocked by this issue.
// Returns issues that cannot be closed until the current issue is closed.
// GET /repos/{owner}/{repo}/issues/{index}/blocks
func (c *Client) MyListIssueBlocking(owner, repo string, index int64) ([]*forgejo.Issue, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/blocks", owner, repo, index)
var issues []*forgejo.Issue
err := c.sendSimpleRequest("GET", endpoint, nil, &issues)
return issues, err
}
// MyAddIssueBlocking blocks the issue given in the body by the issue in path.
// Creates a relationship where the current issue (index) blocks another issue (blocked).
// This means the current issue must be closed before the blocked issue can be closed.
// POST /repos/{owner}/{repo}/issues/{index}/blocks
func (c *Client) MyAddIssueBlocking(owner, repo string, index int64, blocked types.MyIssueMeta) (*forgejo.Issue, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/blocks", owner, repo, index)
var issue *forgejo.Issue
err := c.sendSimpleRequest("POST", endpoint, blocked, &issue)
return issue, err
}
// MyRemoveIssueBlocking unblocks the issue given in the body by the issue in path.
// Removes the relationship where the current issue blocks another issue.
// DELETE /repos/{owner}/{repo}/issues/{index}/blocks
func (c *Client) MyRemoveIssueBlocking(owner, repo string, index int64, blocked types.MyIssueMeta) (*forgejo.Issue, error) {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/blocks", owner, repo, index)
var issue *forgejo.Issue
err := c.sendSimpleRequest("DELETE", endpoint, blocked, &issue)
return issue, err
}
```
--------------------------------------------------------------------------------
/.forgejo/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: release
on:
push:
tags:
- "v*"
jobs:
release:
name: Create Release
runs-on: any
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ">=1.23"
- name: Restore cache
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run test
run: go test -v ./...
- name: Overwrite version info
run: sed -i "s/dev-test/${GITHUB_REF_NAME}/" types/version.go
- name: Prepare docker multiarch build
uses: https://github.com/docker/setup-qemu-action@v3
- name: Setup docker multiarch build
uses: https://github.com/docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Docker - login to hub
uses: https://github.com/docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Build binary for amd64 linux
run: |
GOARCH=amd64 GOOS=linux go build -o forgejo-mcp
cp forgejo-mcp forgejo-mcp.linux.amd64
sha1sum forgejo-mcp.linux.amd64 > forgejo-mcp.linux.amd64.sha1
- name: Build docker image for amd64
uses: https://github.com/docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
pull: true
tags: ronmi/forgejo-mcp:amd64
- name: Build binary for arm64 linux
run: |
GOARCH=arm64 GOOS=linux go build -o forgejo-mcp
cp forgejo-mcp forgejo-mcp.linux.arm64
sha1sum forgejo-mcp.linux.arm64 > forgejo-mcp.linux.arm64.sha1
- name: Build docker image for arm64
uses: https://github.com/docker/build-push-action@v6
with:
context: .
platforms: linux/arm64
push: true
pull: true
tags: ronmi/forgejo-mcp:arm64
- name: Build binary for amd64 darwin
run: |
GOARCH=amd64 GOOS=darwin go build -o forgejo-mcp.darwin.amd64
sha1sum forgejo-mcp.darwin.amd64 > forgejo-mcp.darwin.amd64.sha1
- name: Build binary for arm64 darwin
run: |
GOARCH=arm64 GOOS=darwin go build -o forgejo-mcp.darwin.arm64
sha1sum forgejo-mcp.darwin.arm64 > forgejo-mcp.darwin.arm64.sha1
- name: Build binary for amd64 windows
run: |
GOARCH=amd64 GOOS=windows go build -o forgejo-mcp.amd64.exe
sha1sum forgejo-mcp.amd64.exe > forgejo-mcp.amd64.exe.sha1
- name: Build binary for arm64 windows
run: |
GOARCH=arm64 GOOS=windows go build -o forgejo-mcp.arm64.exe
sha1sum forgejo-mcp.arm64.exe > forgejo-mcp.arm64.exe.sha1
- name: Create multiarch image
run: |
docker buildx imagetools create -t ronmi/forgejo-mcp ronmi/forgejo-mcp:arm64 ronmi/forgejo-mcp:amd64
- name: Update readme to docker hub
uses: https://github.com/peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
repository: ronmi/forgejo-mcp
- name: Create Release
uses: https://github.com/softprops/action-gh-release@v1
with:
files: |
forgejo-mcp.linux.amd64
forgejo-mcp.linux.arm64
forgejo-mcp.amd64.exe
forgejo-mcp.arm64.exe
forgejo-mcp.darwin.amd64
forgejo-mcp.darwin.arm64
forgejo-mcp.linux.amd64.sha1
forgejo-mcp.linux.arm64.sha1
forgejo-mcp.amd64.exe.sha1
forgejo-mcp.arm64.exe.sha1
forgejo-mcp.darwin.amd64.sha1
forgejo-mcp.darwin.arm64.sha1
token: ${{ secrets.FORGEJO_AUTH_TOKEN }}
```
--------------------------------------------------------------------------------
/types/issues.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"fmt"
"strings"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
// Issue represents an issue response with embedded SDK issue
// Used by endpoints:
// - POST /repos/{owner}/{repo}/issues (create)
// - PATCH /repos/{owner}/{repo}/issues/{index} (edit)
type Issue struct {
*forgejo.Issue
}
// ToMarkdown renders issue with title, state, assignees, labels and basic info
// Example: **#123 Fix login bug** (open)
// Author: johndoe
// Assignees: [alice bob]
// Labels: [bug priority-high]
// Milestone: v1.0.0
// Due: 2024-12-31
//
// The login page crashes when...
func (i *Issue) ToMarkdown() string {
if i.Issue == nil {
return "*Invalid issue*"
}
markdown := fmt.Sprintf("**#%d %s** (%s)\n", i.Index, i.Title, i.State)
if i.Poster != nil {
markdown += "Author: " + i.Poster.UserName + "\n"
}
if len(i.Assignees) > 0 {
assignees := make([]string, len(i.Assignees))
for j, assignee := range i.Assignees {
assignees[j] = assignee.UserName
}
markdown += "Assignees: " + fmt.Sprintf("%v", assignees) + "\n"
}
if len(i.Labels) > 0 {
labelNames := make([]string, len(i.Labels))
for j, label := range i.Labels {
labelNames[j] = label.Name
}
markdown += "Labels: " + fmt.Sprintf("%v", labelNames) + "\n"
}
if i.Milestone != nil {
markdown += "Milestone: " + i.Milestone.Title + "\n"
}
if i.Deadline != nil {
markdown += "Due: " + i.Deadline.Format("2006-01-02") + "\n"
}
if i.Body != "" {
markdown += "\n" + i.Body
}
return markdown
}
// Comment represents a comment response with embedded SDK comment
// Used by endpoints:
// - POST /repos/{owner}/{repo}/issues/{index}/comments (create)
type Comment struct {
*forgejo.Comment
}
// ToMarkdown renders comment with author, timestamp and content
// Example: **alice** (2024-01-15 14:30)
// I think the issue is in the authentication module...
func (c *Comment) ToMarkdown() string {
if c.Comment == nil {
return "*Invalid comment*"
}
markdown := fmt.Sprintf("Comment#%d", c.ID)
if c.Poster != nil {
markdown += " **" + c.Poster.UserName + "**"
}
if !c.Created.IsZero() {
markdown += " (" + c.Created.Format("2006-01-02 15:04") + ")"
}
if c.Body != "" {
markdown += "\n" + c.Body
}
return markdown
}
// IssueList represents a list of issues optimized for list display
// Used by list_repo_issues endpoint to show essential information only
type IssueList []*forgejo.Issue
// ToMarkdown renders issues with essential information for quick scanning
// Shows: Index, Title, State, Assignees, Labels, Updated time, Comments count
// Example per issue:
// #123 Fix login bug (open) | [testuser] | [bug priority-high] | 2024-01-15 | 5 comments
func (il IssueList) ToMarkdown() string {
if len(il) == 0 {
return "*No issues found*"
}
markdown := ""
for _, issue := range il {
if issue == nil {
continue
}
// Index, Title, and State
line := fmt.Sprintf("#%d %s (%s)", issue.Index, issue.Title, issue.State)
// Assignees
if len(issue.Assignees) > 0 {
assigneeNames := make([]string, len(issue.Assignees))
for i, assignee := range issue.Assignees {
assigneeNames[i] = assignee.UserName
}
line += " | [" + strings.Join(assigneeNames, " ") + "]"
}
// Labels
if len(issue.Labels) > 0 {
labelNames := make([]string, len(issue.Labels))
for i, label := range issue.Labels {
labelNames[i] = label.Name
}
line += " | [" + strings.Join(labelNames, " ") + "]"
}
// Updated time
if !issue.Updated.IsZero() {
line += " | " + issue.Updated.Format("2006-01-02")
}
// Comments count
line += fmt.Sprintf(" | %d", issue.Comments)
markdown += line + "\n"
}
return markdown
}
```
--------------------------------------------------------------------------------
/types/release_test.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"testing"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
func TestRelease_ToMarkdown(t *testing.T) {
created := testTime()
tests := []struct {
name string
release *Release
required []string
}{
{
name: "complete release with all fields",
release: &Release{
Release: &forgejo.Release{
TagName: "v1.0.0",
Title: "Major Release",
Note: "This release includes new authentication system and bug fixes",
IsDraft: false,
IsPrerelease: true,
CreatedAt: created,
},
},
required: []string{"v1.0.0", "Major Release", "PRERELEASE", "2024-01-15", "This release includes new authentication system"},
},
{
name: "nil release",
release: &Release{Release: nil},
required: []string{"Invalid release"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.release.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestReleaseList_ToMarkdown(t *testing.T) {
created := testTime()
tests := []struct {
name string
releases ReleaseList
required []string
}{
{
name: "multiple releases with complete information",
releases: ReleaseList{
&Release{
Release: &forgejo.Release{
TagName: "v1.0.0",
Title: "Major Release",
Note: "New features",
IsPrerelease: true,
CreatedAt: created,
},
},
&Release{
Release: &forgejo.Release{
TagName: "v0.9.0",
Title: "Beta Release",
CreatedAt: created,
},
},
},
required: []string{"1.", "v1.0.0", "Major Release", "PRERELEASE", "2.", "v0.9.0", "Beta Release"},
},
{
name: "empty release list",
releases: ReleaseList{},
required: []string{"No releases found"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.releases.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestAttachment_ToMarkdown(t *testing.T) {
tests := []struct {
name string
attachment *Attachment
required []string
}{
{
name: "complete attachment with all fields",
attachment: &Attachment{
Attachment: &forgejo.Attachment{
Name: "document.pdf",
Size: 1024,
DownloadURL: "https://git.example.com/attachments/123",
},
},
required: []string{"document.pdf", "1024 bytes", "Download", "https://git.example.com/attachments/123"},
},
{
name: "nil attachment",
attachment: &Attachment{Attachment: nil},
required: []string{"Invalid attachment"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.attachment.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestAttachmentList_ToMarkdown(t *testing.T) {
tests := []struct {
name string
attachments AttachmentList
required []string
}{
{
name: "multiple attachments with complete information",
attachments: AttachmentList{
&Attachment{
Attachment: &forgejo.Attachment{
Name: "document.pdf",
Size: 1024,
DownloadURL: "https://git.example.com/attachments/123",
},
},
&Attachment{
Attachment: &forgejo.Attachment{
Name: "screenshot.png",
Size: 2048,
DownloadURL: "https://git.example.com/attachments/124",
},
},
},
required: []string{"document.pdf", "1024 bytes", "screenshot.png", "2048 bytes", "Download"},
},
{
name: "empty attachment list",
attachments: AttachmentList{},
required: []string{"No attachments found"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.attachments.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
```
--------------------------------------------------------------------------------
/cmd/lib.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package cmd
import (
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/tools/action"
"github.com/raohwork/forgejo-mcp/tools/issue"
"github.com/raohwork/forgejo-mcp/tools/label"
"github.com/raohwork/forgejo-mcp/tools/milestone"
"github.com/raohwork/forgejo-mcp/tools/pullreq"
"github.com/raohwork/forgejo-mcp/tools/release"
"github.com/raohwork/forgejo-mcp/tools/repo"
"github.com/raohwork/forgejo-mcp/tools/wiki"
"github.com/raohwork/forgejo-mcp/types"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func registerCommands(s *mcp.Server, cl *tools.Client) {
// Issue tools
tools.Register(s, &issue.ListRepoIssuesImpl{Client: cl})
tools.Register(s, &issue.GetIssueImpl{Client: cl})
tools.Register(s, &issue.CreateIssueImpl{Client: cl})
tools.Register(s, &issue.EditIssueImpl{Client: cl})
// Issue label tools
tools.Register(s, &issue.AddIssueLabelsImpl{Client: cl})
tools.Register(s, &issue.RemoveIssueLabelImpl{Client: cl})
tools.Register(s, &issue.ReplaceIssueLabelsImpl{Client: cl})
// Issue comment tools
tools.Register(s, &issue.ListIssueCommentsImpl{Client: cl})
tools.Register(s, &issue.CreateIssueCommentImpl{Client: cl})
tools.Register(s, &issue.EditIssueCommentImpl{Client: cl})
tools.Register(s, &issue.DeleteIssueCommentImpl{Client: cl})
// Issue attachment tools
tools.Register(s, &issue.ListIssueAttachmentsImpl{Client: cl})
tools.Register(s, &issue.DeleteIssueAttachmentImpl{Client: cl})
tools.Register(s, &issue.EditIssueAttachmentImpl{Client: cl})
// Issue dependency tools
tools.Register(s, &issue.ListIssueDependenciesImpl{Client: cl})
tools.Register(s, &issue.AddIssueDependencyImpl{Client: cl})
tools.Register(s, &issue.RemoveIssueDependencyImpl{Client: cl})
// Issue blocking tools
tools.Register(s, &issue.ListIssueBlockingImpl{Client: cl})
tools.Register(s, &issue.AddIssueBlockingImpl{Client: cl})
tools.Register(s, &issue.RemoveIssueBlockingImpl{Client: cl})
// Label tools
tools.Register(s, &label.ListRepoLabelsImpl{Client: cl})
tools.Register(s, &label.CreateLabelImpl{Client: cl})
tools.Register(s, &label.EditLabelImpl{Client: cl})
tools.Register(s, &label.DeleteLabelImpl{Client: cl})
// Milestone tools
tools.Register(s, &milestone.ListRepoMilestonesImpl{Client: cl})
tools.Register(s, &milestone.CreateMilestoneImpl{Client: cl})
tools.Register(s, &milestone.EditMilestoneImpl{Client: cl})
tools.Register(s, &milestone.DeleteMilestoneImpl{Client: cl})
// Release tools
tools.Register(s, &release.ListReleasesImpl{Client: cl})
tools.Register(s, &release.CreateReleaseImpl{Client: cl})
tools.Register(s, &release.EditReleaseImpl{Client: cl})
tools.Register(s, &release.DeleteReleaseImpl{Client: cl})
// Release attachment tools
tools.Register(s, &release.ListReleaseAttachmentsImpl{Client: cl})
tools.Register(s, &release.EditReleaseAttachmentImpl{Client: cl})
tools.Register(s, &release.DeleteReleaseAttachmentImpl{Client: cl})
// Pull request tools
tools.Register(s, &pullreq.ListPullRequestsImpl{Client: cl})
tools.Register(s, &pullreq.GetPullRequestImpl{Client: cl})
tools.Register(s, &pullreq.CreatePullRequestImpl{Client: cl})
// Repository tools
tools.Register(s, &repo.SearchRepositoriesImpl{Client: cl})
tools.Register(s, &repo.ListMyRepositoriesImpl{Client: cl})
tools.Register(s, &repo.ListOrgRepositoriesImpl{Client: cl})
tools.Register(s, &repo.GetRepositoryImpl{Client: cl})
// Wiki tools
tools.Register(s, &wiki.GetWikiPageImpl{Client: cl})
tools.Register(s, &wiki.CreateWikiPageImpl{Client: cl})
tools.Register(s, &wiki.EditWikiPageImpl{Client: cl})
tools.Register(s, &wiki.DeleteWikiPageImpl{Client: cl})
tools.Register(s, &wiki.ListWikiPagesImpl{Client: cl})
// Action tools
tools.Register(s, &action.ListActionTasksImpl{Client: cl})
}
func createServer(cl *tools.Client) *mcp.Server {
server := mcp.NewServer(&mcp.Implementation{
Title: "Forgejo MCP Server",
Version: types.VERSION[1:], // strip leading 'v'
}, &mcp.ServerOptions{
PageSize: 50,
Instructions: "An MCP server to interact with repositories on a Forgejo/Gitea instance.",
})
registerCommands(server, cl)
return server
}
```
--------------------------------------------------------------------------------
/types/repo_test.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package types
import (
"testing"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
)
func TestRepository_ToMarkdown(t *testing.T) {
tests := []struct {
name string
repo *Repository
required []string
}{
{
name: "complete repository with all fields",
repo: &Repository{
Repository: &forgejo.Repository{
FullName: "owner/repo-name",
Description: "A sample repository for testing purposes",
Private: true,
Fork: true,
Template: false,
Stars: 42,
Forks: 7,
OpenIssues: 3,
OpenPulls: 1,
HTMLURL: "https://git.example.com/owner/repo-name",
},
},
required: []string{"owner/repo-name", "PRIVATE", "FORK", "A sample repository for testing purposes", "Stars: 42", "Forks: 7", "Issues: 3", "PRs: 1", "View Repository", "https://git.example.com/owner/repo-name"},
},
{
name: "nil repository",
repo: &Repository{Repository: nil},
required: []string{"Invalid repository"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.repo.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestRepositoryList_ToMarkdown(t *testing.T) {
tests := []struct {
name string
repositories RepositoryList
required []string
}{
{
name: "multiple repositories with complete information",
repositories: RepositoryList{
&Repository{
Repository: &forgejo.Repository{
FullName: "owner/repo-name",
Description: "A sample repository",
Private: true,
Fork: true,
Stars: 42,
Forks: 7,
OpenIssues: 3,
OpenPulls: 1,
},
},
&Repository{
Repository: &forgejo.Repository{
FullName: "owner/another-repo",
Description: "Another repository",
Stars: 15,
Forks: 2,
},
},
},
required: []string{"1.", "owner/repo-name", "PRIVATE", "FORK", "2.", "owner/another-repo"},
},
{
name: "empty repository list",
repositories: RepositoryList{},
required: []string{"No repositories found"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.repositories.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestPullRequest_ToMarkdown(t *testing.T) {
tests := []struct {
name string
pr *PullRequest
required []string
}{
{
name: "complete pull request with all fields",
pr: &PullRequest{
PullRequest: &forgejo.PullRequest{
Index: 42,
Title: "Add user authentication",
Body: "This PR implements OAuth2 authentication system",
State: "open",
Poster: testUser(),
Head: &forgejo.PRBranchInfo{
Name: "feature/auth",
},
Base: &forgejo.PRBranchInfo{
Name: "main",
},
},
},
required: []string{"#42", "Add user authentication", "open", "testuser", "feature/auth", "main", "This PR implements OAuth2 authentication"},
},
{
name: "nil pull request",
pr: &PullRequest{PullRequest: nil},
required: []string{"Invalid pull request"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.pr.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
func TestPullRequestList_ToMarkdown(t *testing.T) {
tests := []struct {
name string
prs PullRequestList
required []string
}{
{
name: "multiple pull requests with complete information",
prs: PullRequestList{
&PullRequest{
PullRequest: &forgejo.PullRequest{
Index: 42,
Title: "Add user authentication",
State: "open",
Poster: testUser(),
},
},
&PullRequest{
PullRequest: &forgejo.PullRequest{
Index: 41,
Title: "Fix database connection",
State: "merged",
},
},
},
required: []string{"1.", "#42", "Add user authentication", "2.", "#41", "Fix database connection"},
},
{
name: "empty pull request list",
prs: PullRequestList{},
required: []string{"No pull requests found"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.prs.ToMarkdown()
assertContains(t, output, tt.required)
})
}
}
```
--------------------------------------------------------------------------------
/tools/pullreq/create.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package pullreq
import (
"context"
"fmt"
"time"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// CreatePullRequestParams defines the parameters for the create_pull_request tool.
// It captures the branches, title, description, and optional metadata for the new pull request.
type CreatePullRequestParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Head identifies the source branch (optionally `owner:branch`) to merge from.
Head string `json:"head"`
// Base identifies the target branch to merge into.
Base string `json:"base"`
// Title is the pull request title.
Title string `json:"title"`
// Body is the pull request description in Markdown.
Body string `json:"body,omitempty"`
// Assignee assigns the pull request to a single user.
Assignee string `json:"assignee,omitempty"`
// Assignees assigns multiple users to the pull request.
Assignees []string `json:"assignees,omitempty"`
// Milestone is the milestone ID associated with the pull request.
Milestone int `json:"milestone,omitempty"`
// Labels is a slice of label IDs to attach to the pull request.
Labels []int `json:"labels,omitempty"`
// DueDate is the optional deadline for the pull request.
DueDate time.Time `json:"due_date"`
}
// CreatePullRequestImpl implements the MCP tool for creating a new pull request.
// This is a non-idempotent operation that opens a new pull request via the Forgejo SDK.
type CreatePullRequestImpl struct {
Client *tools.Client
}
// Definition describes the `create_pull_request` tool. It requires `owner`, `repo`,
// `head`, `base`, and a `title`. Repeated calls will create multiple pull requests.
func (CreatePullRequestImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "create_pull_request",
Title: "Create Pull Request",
Description: "Create a new pull request in a repository. Provide the source and target branches, title, body, and optional metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: false,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"head": {
Type: "string",
Description: "Source branch to merge from (optionally include owner as owner:branch)",
},
"base": {
Type: "string",
Description: "Target branch to merge into",
},
"title": {
Type: "string",
Description: "Pull request title",
},
"body": {
Type: "string",
Description: "Pull request description (markdown supported) (optional)",
},
"assignee": {
Type: "string",
Description: "Username of the primary assignee (optional)",
},
"assignees": {
Type: "array",
Items: &jsonschema.Schema{
Type: "string",
},
Description: "Additional usernames to assign to this pull request (optional)",
},
"milestone": {
Type: "integer",
Description: "Milestone ID to associate with this pull request (optional)",
},
"labels": {
Type: "array",
Items: &jsonschema.Schema{
Type: "integer",
},
Description: "Array of label IDs to attach to this pull request (optional)",
},
"due_date": {
Type: "string",
Description: "Pull request due date in ISO 8601 format (e.g., '2024-12-31T23:59:59Z') (optional)",
Format: "date-time",
},
},
Required: []string{"owner", "repo", "head", "base", "title"},
},
}
}
// Handler implements the logic for creating a pull request. It calls the Forgejo SDK's
// `CreatePullRequest` function and returns the details of the newly created pull request.
func (impl CreatePullRequestImpl) Handler() mcp.ToolHandlerFor[CreatePullRequestParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args CreatePullRequestParams) (*mcp.CallToolResult, any, error) {
p := args
opt := forgejo.CreatePullRequestOption{
Head: p.Head,
Base: p.Base,
Title: p.Title,
Body: p.Body,
Assignee: p.Assignee,
Assignees: p.Assignees,
}
if p.Milestone > 0 {
opt.Milestone = int64(p.Milestone)
}
if len(p.Labels) > 0 {
opt.Labels = make([]int64, len(p.Labels))
for i, label := range p.Labels {
opt.Labels[i] = int64(label)
}
}
if !p.DueDate.IsZero() {
opt.Deadline = &p.DueDate
}
pr, _, err := impl.Client.CreatePullRequest(p.Owner, p.Repo, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to create pull request: %w", err)
}
prWrapper := &types.PullRequest{PullRequest: pr}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: prWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/client.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package tools
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/raohwork/forgejo-mcp/types"
)
var UserAgent = "Forgejo-MCP/" + types.VERSION
// Client wraps the Forgejo SDK client with additional functionality for
// unsupported API endpoints. It provides methods for JSON requests and
// multipart file uploads with manual authentication.
type Client struct {
*forgejo.Client
cl *http.Client
base string
token string
}
// NewClient creates a new Client instance with extended functionality beyond
// the standard Forgejo SDK. This client supports custom API endpoints that
// are not available in the SDK, such as issue dependencies, wiki pages,
// and Forgejo Actions.
//
// Parameters:
// - base: Forgejo server base URL (e.g., "https://git.example.com")
// - token: API access token for authentication
// - version: Forgejo version string to skip version check, empty to auto-detect
// - cl: HTTP client to use, nil for http.DefaultClient
//
// The client uses manual token authentication for custom endpoints while
// preserving full SDK functionality for supported operations.
func NewClient(base, token, version string, cl *http.Client) (*Client, error) {
if cl == nil {
cl = http.DefaultClient
}
opts := make([]forgejo.ClientOption, 0, 4)
opts = append(
opts,
forgejo.SetHTTPClient(cl),
forgejo.SetToken(token),
forgejo.SetUserAgent(UserAgent),
)
if version != "" {
opts = append(opts, forgejo.SetForgejoVersion(version))
}
sdk, err := forgejo.NewClient(base, opts...)
if err != nil {
return nil, err
}
return &Client{
Client: sdk,
cl: cl,
base: base,
token: token,
}, nil
}
// sendSimpleRequest handles pure JSON API requests
// method: HTTP method (GET, POST, PATCH, DELETE)
// endpoint: API endpoint path (relative to base URL)
// paramObj: request parameter object (JSON serialized), can be nil for GET/DELETE
// respObj: response data receiver object (JSON deserialized)
func (c *Client) sendSimpleRequest(method, endpoint string, paramObj, respObj any) error {
// Build complete URL
u, err := url.Parse(c.base + endpoint)
if err != nil {
return fmt.Errorf("invalid URL: %w", err)
}
// Prepare request body
var body io.Reader
if paramObj != nil {
jsonData, err := json.Marshal(paramObj)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
body = bytes.NewReader(jsonData)
}
// Create HTTP request
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Accept", "application/json")
if paramObj != nil {
req.Header.Set("Content-Type", "application/json")
}
// Set authentication header manually
if c.token != "" {
req.Header.Set("Authorization", "token "+c.token)
}
// Send request
resp, err := c.cl.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// Check HTTP status
if resp.StatusCode >= 400 {
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
// Parse JSON response
if err := json.NewDecoder(resp.Body).Decode(respObj); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
return nil
}
// sendUploadRequest handles file upload requests (multipart/form-data)
// endpoint: API endpoint path (fixed to use POST)
// filename: upload file name
// file: file content
// extraFields: additional form fields
// respObj: response data receiver object (JSON deserialized)
func (c *Client) sendUploadRequest(endpoint, filename string, file io.Reader, extraFields map[string]string, respObj any) error {
// Build complete URL
u, err := url.Parse(c.base + endpoint)
if err != nil {
return fmt.Errorf("invalid URL: %w", err)
}
// Create multipart form data
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// Add extra fields
for key, value := range extraFields {
if err := writer.WriteField(key, value); err != nil {
return fmt.Errorf("failed to write field %s: %w", key, err)
}
}
// Add file
part, err := writer.CreateFormFile("attachment", filename)
if err != nil {
return fmt.Errorf("failed to create form file: %w", err)
}
if _, err := io.Copy(part, file); err != nil {
return fmt.Errorf("failed to copy file content: %w", err)
}
if err := writer.Close(); err != nil {
return fmt.Errorf("failed to close multipart writer: %w", err)
}
// Create HTTP request
req, err := http.NewRequest("POST", u.String(), &buf)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", writer.FormDataContentType())
// Set authentication header manually
if c.token != "" {
req.Header.Set("Authorization", "token "+c.token)
}
// Send request
resp, err := c.cl.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// Check HTTP status
if resp.StatusCode >= 400 {
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
// Parse JSON response
if err := json.NewDecoder(resp.Body).Decode(respObj); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
return nil
}
```
--------------------------------------------------------------------------------
/tools/pullreq/list.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package pullreq
import (
"context"
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListPullRequestsParams defines the parameters for the list_pull_requests tool.
// It includes options for filtering, sorting, and paginating pull requests.
type ListPullRequestsParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// State filters pull requests by their state (e.g., 'open', 'closed').
State string `json:"state,omitempty"`
// Sort specifies the sort order for the results.
Sort string `json:"sort,omitempty"`
// Direction specifies the sort direction (asc or desc).
Direction string `json:"direction,omitempty"`
// Milestone filters pull requests by a milestone title.
Milestone string `json:"milestone,omitempty"`
// Labels filters pull requests by a list of label names.
Labels []string `json:"labels,omitempty"`
// Page is the page number for pagination.
Page int `json:"page,omitempty"`
// Limit is the number of pull requests to return per page.
Limit int `json:"limit,omitempty"`
}
// ListPullRequestsImpl implements the read-only MCP tool for listing pull requests.
// This is a safe, idempotent operation that uses the Forgejo SDK to fetch a list
// of pull requests with powerful filtering and sorting capabilities.
type ListPullRequestsImpl struct {
Client *tools.Client
}
// Definition describes the `list_pull_requests` tool. It requires `owner` and `repo`
// and supports a rich set of optional parameters for filtering and sorting.
// It is marked as a safe, read-only operation.
func (ListPullRequestsImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_pull_requests",
Title: "List Pull Requests",
Description: "List pull requests in a repository. Returns PR information including title, state, author, and metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"state": {
Type: "string",
Description: "PR state filter: 'open', 'closed', or 'all' (optional, defaults to 'open')",
Enum: []any{"open", "closed", "all"},
},
"sort": {
Type: "string",
Description: "Sort order: 'created', 'updated', 'popularity' (comment count), or 'long-running' (age, filtering by time updated) (optional, defaults to 'created')",
Enum: []any{"created", "updated", "popularity", "long-running"},
},
"direction": {
Type: "string",
Description: "Sort direction: 'asc' or 'desc' (optional, defaults to 'desc')",
Enum: []any{"asc", "desc"},
},
"milestone": {
Type: "string",
Description: "Filter by milestone title (optional)",
},
"labels": {
Type: "array",
Items: &jsonschema.Schema{
Type: "string",
},
Description: "Filter by label names (optional)",
},
"page": {
Type: "integer",
Description: "Page number for pagination (optional, defaults to 1)",
Minimum: tools.Float64Ptr(1),
},
"limit": {
Type: "integer",
Description: "Number of pull requests per page (optional, defaults to 20, max 50)",
Minimum: tools.Float64Ptr(1),
Maximum: tools.Float64Ptr(50),
},
},
Required: []string{"owner", "repo"},
},
}
}
// Handler implements the logic for listing pull requests. It calls the Forgejo SDK's
// `ListRepoPullRequests` function with the provided filters and formats the results
// into a markdown table.
func (impl ListPullRequestsImpl) Handler() mcp.ToolHandlerFor[ListPullRequestsParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListPullRequestsParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.ListPullRequestsOptions{}
if p.State != "" {
opt.State = forgejo.StateType(p.State)
}
if p.Sort != "" {
opt.Sort = p.Sort
}
// Note: Direction is not supported in the SDK options
// Note: Labels filtering is not directly supported in SDK
// Note: Milestone string name is not supported, only ID
if p.Page > 0 {
opt.Page = p.Page
}
if p.Limit > 0 {
opt.PageSize = p.Limit
}
// Call SDK
prs, _, err := impl.Client.ListRepoPullRequests(p.Owner, p.Repo, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to list pull requests: %w", err)
}
// Convert to our types and format
var content string
if len(prs) == 0 {
content = "No pull requests found matching the criteria."
} else {
// Convert PRs to our type
prList := make(types.PullRequestList, len(prs))
for i, pr := range prs {
prList[i] = &types.PullRequest{PullRequest: pr}
}
content = fmt.Sprintf("Found %d pull requests\n\n%s",
len(prs), prList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/issue/label.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package issue
import (
"context"
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// AddIssueLabelsParams defines the parameters for the add_issue_labels tool.
// It specifies the issue and the label IDs to be added.
type AddIssueLabelsParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
// Labels is a slice of label IDs to add to the issue.
Labels []int `json:"labels"`
}
// AddIssueLabelsImpl implements the MCP tool for adding labels to an issue.
// This is an idempotent operation that uses the Forgejo SDK to associate one
// or more existing labels with an issue.
type AddIssueLabelsImpl struct {
Client *tools.Client
}
// Definition describes the `add_issue_labels` tool. It requires the issue's `index`
// and an array of `labels` (IDs). It is marked as idempotent.
func (AddIssueLabelsImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "add_issue_labels",
Title: "Add Issue Labels",
Description: "Add labels to an existing issue.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"labels": {
Type: "array",
Items: &jsonschema.Schema{
Type: "integer",
},
Description: "Array of label IDs to add to this issue",
MinItems: tools.IntPtr(1),
},
},
Required: []string{"owner", "repo", "index", "labels"},
},
}
}
// Handler implements the logic for adding labels to an issue. It calls the
// Forgejo SDK's `AddIssueLabels` function. It will return an error if the issue
// or any of the label IDs are not found.
func (impl AddIssueLabelsImpl) Handler() mcp.ToolHandlerFor[AddIssueLabelsParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args AddIssueLabelsParams) (*mcp.CallToolResult, any, error) {
p := args
// Convert int labels to int64
labelIDs := make([]int64, len(p.Labels))
for i, label := range p.Labels {
labelIDs[i] = int64(label)
}
opt := forgejo.IssueLabelsOption{
Labels: labelIDs,
}
labels, _, err := impl.Client.AddIssueLabels(p.Owner, p.Repo, int64(p.Index), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to add labels: %w", err)
}
// Convert to our types
var labelsMarkdown string
for _, label := range labels {
labelWrapper := &types.Label{Label: label}
labelsMarkdown += labelWrapper.ToMarkdown() + "\n"
}
content := fmt.Sprintf("Added %d labels to issue #%d\n\n%s", len(labels), p.Index, labelsMarkdown)
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// RemoveIssueLabelParams defines the parameters for the remove_issue_label tool.
// It specifies the issue and the single label ID to be removed.
type RemoveIssueLabelParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
// Label is the ID of the label to remove from the issue.
Label int `json:"label"`
}
// RemoveIssueLabelImpl implements the MCP tool for removing a label from an issue.
// This is an idempotent operation that uses the Forgejo SDK to disassociate a
// label from an issue.
type RemoveIssueLabelImpl struct {
Client *tools.Client
}
// Definition describes the `remove_issue_label` tool. It requires the issue's
// `index` and a single `label` ID to remove. It is marked as idempotent.
func (RemoveIssueLabelImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "remove_issue_label",
Title: "Remove Issue Label",
Description: "Remove a specific label from an issue.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"label": {
Type: "integer",
Description: "Label ID to remove from this issue",
},
},
Required: []string{"owner", "repo", "index", "label"},
},
}
}
// Handler implements the logic for removing a label from an issue. It calls the
// Forgejo SDK's `DeleteIssueLabel` function. On success, it returns a simple
// text confirmation. It will return an error if the issue or label is not found.
func (impl RemoveIssueLabelImpl) Handler() mcp.ToolHandlerFor[RemoveIssueLabelParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args RemoveIssueLabelParams) (*mcp.CallToolResult, any, error) {
p := args
_, err := impl.Client.DeleteIssueLabel(p.Owner, p.Repo, int64(p.Index), int64(p.Label))
if err != nil {
return nil, nil, fmt.Errorf("failed to remove label: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("Label %d successfully removed from issue #%d.", p.Label, p.Index),
},
},
}, nil, nil
}
}
// ReplaceIssueLabelsParams defines the parameters for the replace_issue_labels tool.
// It specifies the issue and the new set of label IDs.
type ReplaceIssueLabelsParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
// Labels is a slice of label IDs that will replace all existing labels on the issue.
Labels []int `json:"labels"`
}
// ReplaceIssueLabelsImpl implements the MCP tool for replacing all labels on an issue.
// This is an idempotent operation that uses the Forgejo SDK to set the definitive
// list of labels for an issue.
type ReplaceIssueLabelsImpl struct {
Client *tools.Client
}
// Definition describes the `replace_issue_labels` tool. It requires the issue's
// `index` and an array of `labels` (IDs) to apply. It is marked as idempotent.
func (ReplaceIssueLabelsImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "replace_issue_labels",
Title: "Replace Issue Labels",
Description: "Replace all labels on an issue with a new set of labels.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"labels": {
Type: "array",
Items: &jsonschema.Schema{
Type: "integer",
},
Description: "Array of label IDs to set on this issue (replaces all existing labels)",
},
},
Required: []string{"owner", "repo", "index", "labels"},
},
}
}
// Handler implements the logic for replacing issue labels. It calls the Forgejo
// SDK's `ReplaceIssueLabels` function. It will return an error if the issue or
// any of the label IDs are not found.
func (impl ReplaceIssueLabelsImpl) Handler() mcp.ToolHandlerFor[ReplaceIssueLabelsParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ReplaceIssueLabelsParams) (*mcp.CallToolResult, any, error) {
p := args
// Convert int labels to int64
labelIDs := make([]int64, len(p.Labels))
for i, label := range p.Labels {
labelIDs[i] = int64(label)
}
opt := forgejo.IssueLabelsOption{
Labels: labelIDs,
}
labels, _, err := impl.Client.ReplaceIssueLabels(p.Owner, p.Repo, int64(p.Index), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to replace labels: %w", err)
}
// Convert to our types
var labelsMarkdown string
for _, label := range labels {
labelWrapper := &types.Label{Label: label}
labelsMarkdown += labelWrapper.ToMarkdown() + "\n"
}
content := fmt.Sprintf("Replaced labels for issue #%d with %d labels\n\n%s", p.Index, len(labels), labelsMarkdown)
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/features.md:
--------------------------------------------------------------------------------
```markdown
# Feature List
## Implementation Strategy
- 🟢 **SDK**: Implementation using official SDK
- 🟡 **Custom**: Custom HTTP request implementation (reusing SDK authentication)
- 🔴 **Mixed**: Some features using SDK, some requiring custom implementation
### Label Features
Labels available for a specific repository
- **List Labels** 🟢
- `GET /repos/{owner}/{repo}/labels`
- SDK: `ListRepoLabels(owner, repo string, opt ListLabelsOptions) ([]*Label, *Response, error)`
- **Modify label name, description, and color** 🟢
- `PATCH /repos/{owner}/{repo}/labels/{id}`
- SDK: `EditLabel(owner, repo string, id int64, opt EditLabelOption) (*Label, *Response, error)`
- **Create or delete labels** 🟢
- `POST /repos/{owner}/{repo}/labels`
- SDK: `CreateLabel(owner, repo string, opt CreateLabelOption) (*Label, *Response, error)`
- `DELETE /repos/{owner}/{repo}/labels/{id}`
- SDK: `DeleteLabel(owner, repo string, id int64) (*Response, error)`
### Milestone Features 🟢
- **List Milestones**
- `GET /repos/{owner}/{repo}/milestones`
- SDK: `ListRepoMilestones(owner, repo string, opt ListMilestoneOption) ([]*Milestone, *Response, error)`
- **Create, delete, and modify milestones (including title, due date, and description)**
- `POST /repos/{owner}/{repo}/milestones`
- SDK: `CreateMilestone(owner, repo string, opt CreateMilestoneOption) (*Milestone, *Response, error)`
- `DELETE /repos/{owner}/{repo}/milestones/{id}`
- SDK: `DeleteMilestone(owner, repo string, id int64) (*Response, error)`
- `PATCH /repos/{owner}/{repo}/milestones/{id}`
- SDK: `EditMilestone(owner, repo string, id int64, opt EditMilestoneOption) (*Milestone, *Response, error)`
### Issue Features 🔴
- **List Repository Issues** 🟢
- `GET /repos/{owner}/{repo}/issues`
- SDK: `ListRepoIssues(owner, repo string, opt ListIssueOption) ([]*Issue, *Response, error)`
- Supports filters: state, labels, milestones, assignees, search, date filters
- **Get Specific Issue Details** 🟢
- `GET /repos/{owner}/{repo}/issues/{index}`
- SDK: `GetIssue(owner, repo string, index int64) (*Issue, *Response, error)`
- **List Issue Comments** 🟢
- `GET /repos/{owner}/{repo}/issues/{index}/comments`
- SDK: `ListIssueComments(owner, repo string, index int64, opt ListIssueCommentOptions) ([]*Comment, *Response, error)`
- **Create new issue** 🟢
- `POST /repos/{owner}/{repo}/issues`
- SDK: `CreateIssue(owner, repo string, opt CreateIssueOption) (*Issue, *Response, error)`
- **Comment on existing issue** 🟢
- `POST /repos/{owner}/{repo}/issues/{index}/comments`
- SDK: `CreateIssueComment(owner, repo string, index int64, opt CreateIssueCommentOption) (*Comment, *Response, error)`
- **Close issue** 🟢
- `PATCH /repos/{owner}/{repo}/issues/{index}` (set `state` to `closed`)
- SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
- **Modify issue data** 🟢
- **Description:** `PATCH /repos/{owner}/{repo}/issues/{index}` (modify `body`)
- SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
- **Labels:** 🟢
- `POST /repos/{owner}/{repo}/issues/{index}/labels` (add)
- SDK: `AddIssueLabels(owner, repo string, index int64, opt IssueLabelsOption) ([]*Label, *Response, error)`
- `DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id}` (remove)
- SDK: `DeleteIssueLabel(owner, repo string, index, label int64) (*Response, error)`
- `PUT /repos/{owner}/{repo}/issues/{index}/labels` (replace)
- SDK: `ReplaceIssueLabels(owner, repo string, index int64, opt IssueLabelsOption) ([]*Label, *Response, error)`
- **Assignees:** 🟢 `PATCH /repos/{owner}/{repo}/issues/{index}` (modify `assignees`)
- SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
- **Milestone:** 🟢 `PATCH /repos/{owner}/{repo}/issues/{index}` (modify `milestone`)
- SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
- **Due date:** 🟢 `PATCH /repos/{owner}/{repo}/issues/{index}` (modify `due_date`)
- SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
- **Dependency management:** 🟡
- **Dependencies (issues that block this issue):**
- **Add dependency:** `POST /repos/{owner}/{repo}/issues/{index}/dependencies`
- Custom: Not supported by SDK, requires custom HTTP request
- **List dependencies:** `GET /repos/{owner}/{repo}/issues/{index}/dependencies`
- Custom: Not supported by SDK, requires custom HTTP request
- **Remove dependency:** `DELETE /repos/{owner}/{repo}/issues/{index}/dependencies` (via request body)
- Custom: Not supported by SDK, requires custom HTTP request
- **Blocking (issues blocked by this issue):**
- **Add blocking:** `POST /repos/{owner}/{repo}/issues/{index}/blocks`
- Custom: Not supported by SDK, requires custom HTTP request
- **List blocking:** `GET /repos/{owner}/{repo}/issues/{index}/blocks`
- Custom: Not supported by SDK, requires custom HTTP request
- **Remove blocking:** `DELETE /repos/{owner}/{repo}/issues/{index}/blocks` (via request body)
- Custom: Not supported by SDK, requires custom HTTP request
- **Edit Issue Comments** 🟢
- `PATCH /repos/{owner}/{repo}/issues/comments/{id}`
- SDK: `EditIssueComment(owner, repo string, commentID int64, opt EditIssueCommentOption) (*Comment, *Response, error)`
- **Delete Issue Comments** 🟢
- `DELETE /repos/{owner}/{repo}/issues/comments/{id}`
- SDK: `DeleteIssueComment(owner, repo string, commentID int64) (*Response, error)`
- **Attachment management** 🟡
- **List attachments:** `GET /repos/{owner}/{repo}/issues/{index}/assets`
- Custom: Not supported by SDK, requires custom HTTP request
- **Delete attachment:** `DELETE /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}`
- Custom: Not supported by SDK, requires custom HTTP request
- **Modify attachment:** `PATCH /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}`
- Custom: Not supported by SDK, requires custom HTTP request
### Wiki Features 🟡
- **Query pages**
- `GET /repos/{owner}/{repo}/wiki/page/{pageName}`
- Custom: Not supported by SDK, requires custom HTTP request
- **List pages**
- `GET /repos/{owner}/{repo}/wiki/pages`
- Custom: Not supported by SDK, requires custom HTTP request
- **Create, delete, and modify pages**
- `POST /repos/{owner}/{repo}/wiki/new`
- Custom: Not supported by SDK, requires custom HTTP request
- `DELETE /repos/{owner}/{repo}/wiki/page/{pageName}`
- Custom: Not supported by SDK, requires custom HTTP request
- `PATCH /repos/{owner}/{repo}/wiki/page/{pageName}`
- Custom: Not supported by SDK, requires custom HTTP request
### Release Management 🟢
- **List Releases**
- `GET /repos/{owner}/{repo}/releases`
- SDK: `ListReleases(owner, repo string, opt ListReleasesOptions) ([]*Release, *Response, error)`
- **Create, delete, and modify releases**
- `POST /repos/{owner}/{repo}/releases`
- SDK: `CreateRelease(owner, repo string, opt CreateReleaseOption) (*Release, *Response, error)`
- `DELETE /repos/{owner}/{repo}/releases/{id}`
- SDK: `DeleteRelease(user, repo string, id int64) (*Response, error)`
- `PATCH /repos/{owner}/{repo}/releases/{id}`
- SDK: `EditRelease(owner, repo string, id int64, form EditReleaseOption) (*Release, *Response, error)`
- **Attachment management**
- **List attachments:** `GET /repos/{owner}/{repo}/releases/{id}/assets`
- SDK: `ListReleaseAttachments(user, repo string, release int64, opt ListReleaseAttachmentsOptions) ([]*Attachment, *Response, error)`
- **Delete attachment:** `DELETE /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}`
- SDK: `DeleteReleaseAttachment(user, repo string, release, id int64) (*Response, error)`
- **Modify attachment:** `PATCH /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}`
- SDK: `EditReleaseAttachment(user, repo string, release, attachment int64, form EditAttachmentOptions) (*Attachment, *Response, error)`
### PR Management 🟢
- **List and query PRs**
- `GET /repos/{owner}/{repo}/pulls`
- SDK: `ListRepoPullRequests(owner, repo string, opt ListPullRequestsOptions) ([]*PullRequest, *Response, error)`
- `GET /repos/{owner}/{repo}/pulls/{index}`
- SDK: `GetPullRequest(owner, repo string, index int64) (*PullRequest, *Response, error)`
### Repository Management 🟢
- **List and query repositories**
- `GET /repos/search`
- SDK: `SearchRepos(opt SearchRepoOptions) ([]*Repository, *Response, error)`
- `GET /user/repos`
- SDK: `ListMyRepos(opt ListReposOptions) ([]*Repository, *Response, error)`
- `GET /orgs/{org}/repos`
- SDK: `ListOrgRepos(org string, opt ListOrgReposOptions) ([]*Repository, *Response, error)`
- **Get Specific Repository Information** 🟢
- `GET /repos/{owner}/{repo}`
- SDK: `GetRepo(owner, repo string) (*Repository, *Response, error)`
### Forgejo Actions (CI/CD) 🟡
- **List Action execution tasks**
- `GET /repos/{owner}/{repo}/actions/tasks`
- Custom: Not supported by SDK, requires custom HTTP request
## Summary
- 🟢 **Fully supported (5/7)**: Label, Milestone, Release, PR, Repository management
- 🔴 **Partially supported (1/7)**: Issue features (attachments and dependencies require custom implementation)
- 🟡 **Requires custom implementation (2/7)**: Wiki, Forgejo Actions
**Recommended Hybrid approach**: Approximately 71% of features can use SDK, remaining features require custom HTTP requests.
```
--------------------------------------------------------------------------------
/tools/release/attach.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package release
import (
"context"
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListReleaseAttachmentsParams defines the parameters for the list_release_attachments tool.
// It specifies the release to list attachments from.
type ListReleaseAttachmentsParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// ReleaseID is the unique identifier of the release.
ReleaseID int `json:"release_id"`
}
// ListReleaseAttachmentsImpl implements the read-only MCP tool for listing release attachments.
// This is a safe, idempotent operation that uses the Forgejo SDK to fetch a list
// of all attachments for a specific release.
type ListReleaseAttachmentsImpl struct {
Client *tools.Client
}
// Definition describes the `list_release_attachments` tool. It requires `owner`,
// `repo`, and `release_id`. It is marked as a safe, read-only operation.
func (ListReleaseAttachmentsImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_release_attachments",
Title: "List Release Attachments",
Description: "List all attachments for a specific release.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"release_id": {
Type: "integer",
Description: "Release ID",
},
},
Required: []string{"owner", "repo", "release_id"},
},
}
}
// Handler implements the logic for listing release attachments. It calls the Forgejo
// SDK's `ListReleaseAttachments` function and formats the results into a markdown list.
func (impl ListReleaseAttachmentsImpl) Handler() mcp.ToolHandlerFor[ListReleaseAttachmentsParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListReleaseAttachmentsParams) (*mcp.CallToolResult, any, error) {
p := args
// Call SDK
attachments, _, err := impl.Client.ListReleaseAttachments(p.Owner, p.Repo, int64(p.ReleaseID), forgejo.ListReleaseAttachmentsOptions{})
if err != nil {
return nil, nil, fmt.Errorf("failed to list release attachments: %w", err)
}
// Convert to our types and format
var content string
if len(attachments) == 0 {
content = "No attachments found for this release."
} else {
// Convert attachments to our type
attachmentList := make(types.AttachmentList, len(attachments))
for i, attachment := range attachments {
attachmentList[i] = &types.Attachment{Attachment: attachment}
}
content = fmt.Sprintf("Found %d attachments\n\n%s",
len(attachments), attachmentList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// EditReleaseAttachmentParams defines the parameters for editing a release attachment.
// It specifies the attachment to edit and its new name.
type EditReleaseAttachmentParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// ReleaseID is the unique identifier of the release containing the attachment.
ReleaseID int `json:"release_id"`
// AttachmentID is the unique identifier of the attachment to edit.
AttachmentID int `json:"attachment_id"`
// Name is the new display name for the attachment.
Name string `json:"name"`
}
// EditReleaseAttachmentImpl implements the MCP tool for editing a release attachment.
// This is an idempotent operation that renames an existing attachment using the
// Forgejo SDK.
type EditReleaseAttachmentImpl struct {
Client *tools.Client
}
// Definition describes the `edit_release_attachment` tool. It requires `release_id`,
// `attachment_id`, and a new `name`. It is marked as idempotent.
func (EditReleaseAttachmentImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "edit_release_attachment",
Title: "Edit Release Attachment",
Description: "Edit a release attachment's metadata (like display name).",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"release_id": {
Type: "integer",
Description: "Release ID",
},
"attachment_id": {
Type: "integer",
Description: "Attachment ID to edit",
},
"name": {
Type: "string",
Description: "New display name for the attachment",
},
},
Required: []string{"owner", "repo", "release_id", "attachment_id", "name"},
},
}
}
// Handler implements the logic for editing a release attachment. It calls the Forgejo
// SDK's `EditReleaseAttachment` function. It will return an error if the attachment
// ID is not found.
func (impl EditReleaseAttachmentImpl) Handler() mcp.ToolHandlerFor[EditReleaseAttachmentParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args EditReleaseAttachmentParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.EditAttachmentOptions{
Name: p.Name,
}
// Call SDK
attachment, _, err := impl.Client.EditReleaseAttachment(p.Owner, p.Repo, int64(p.ReleaseID), int64(p.AttachmentID), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to edit release attachment: %w", err)
}
// Convert to our type and format
attachmentWrapper := &types.Attachment{Attachment: attachment}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: attachmentWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// DeleteReleaseAttachmentParams defines the parameters for deleting a release attachment.
// It specifies the attachment to be deleted by its ID.
type DeleteReleaseAttachmentParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// ReleaseID is the unique identifier of the release containing the attachment.
ReleaseID int `json:"release_id"`
// AttachmentID is the unique identifier of the attachment to delete.
AttachmentID int `json:"attachment_id"`
}
// DeleteReleaseAttachmentImpl implements the destructive MCP tool for deleting a release attachment.
// This is an idempotent but irreversible operation that removes an attachment from a
// release using the Forgejo SDK.
type DeleteReleaseAttachmentImpl struct {
Client *tools.Client
}
// Definition describes the `delete_release_attachment` tool. It requires `release_id`
// and `attachment_id`. It is marked as a destructive operation to ensure clients
// can warn the user before execution.
func (DeleteReleaseAttachmentImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "delete_release_attachment",
Title: "Delete Release Attachment",
Description: "Delete an attachment from a release.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(true),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"release_id": {
Type: "integer",
Description: "Release ID",
},
"attachment_id": {
Type: "integer",
Description: "Attachment ID to delete",
},
},
Required: []string{"owner", "repo", "release_id", "attachment_id"},
},
}
}
// Handler implements the logic for deleting a release attachment. It calls the Forgejo
// SDK's `DeleteReleaseAttachment` function. On success, it returns a simple text
// confirmation. It will return an error if the attachment does not exist.
func (impl DeleteReleaseAttachmentImpl) Handler() mcp.ToolHandlerFor[DeleteReleaseAttachmentParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteReleaseAttachmentParams) (*mcp.CallToolResult, any, error) {
p := args
// Call SDK
_, err := impl.Client.DeleteReleaseAttachment(p.Owner, p.Repo, int64(p.ReleaseID), int64(p.AttachmentID))
if err != nil {
return nil, nil, fmt.Errorf("failed to delete release attachment: %w", err)
}
// Return success message
emptyResponse := types.EmptyResponse{}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: emptyResponse.ToMarkdown(),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/issue/attach.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package issue
import (
"context"
"fmt"
"strconv"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListIssueAttachmentsParams defines the parameters for the list_issue_attachments tool.
// It specifies the issue from which to list attachments.
type ListIssueAttachmentsParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
}
// ListIssueAttachmentsImpl implements the read-only MCP tool for listing issue attachments.
// This is a safe, idempotent operation. Note: This feature is not supported by the
// official Forgejo SDK and requires a custom HTTP implementation.
type ListIssueAttachmentsImpl struct {
Client *tools.Client
}
// Definition describes the `list_issue_attachments` tool. It requires `owner`, `repo`,
// and the issue `index`. It is marked as a safe, read-only operation.
func (ListIssueAttachmentsImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_issue_attachments",
Title: "List Issue Attachments",
Description: "List all attachments on an issue. Returns attachment information including names, sizes, and download URLs.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
},
Required: []string{"owner", "repo", "index"},
},
}
}
// Handler implements the logic for listing issue attachments. It performs a custom
// HTTP GET request to the `/repos/{owner}/{repo}/issues/{index}/assets`
// endpoint and formats the results into a markdown list.
func (impl ListIssueAttachmentsImpl) Handler() mcp.ToolHandlerFor[ListIssueAttachmentsParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListIssueAttachmentsParams) (*mcp.CallToolResult, any, error) {
p := args
// List issue attachments using the custom client method
attachments, err := impl.Client.MyListIssueAttachments(p.Owner, p.Repo, int64(p.Index))
if err != nil {
return nil, nil, fmt.Errorf("failed to list issue attachments: %w", err)
}
// Convert to types.AttachmentList for consistent formatting
attachmentList := make(types.AttachmentList, len(attachments))
for i, attachment := range attachments {
attachmentList[i] = &types.Attachment{Attachment: attachment}
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("# Issue #%d Attachments\n\n%s", p.Index, attachmentList.ToMarkdown()),
},
},
}, nil, nil
}
}
// DeleteIssueAttachmentParams defines the parameters for deleting an issue attachment.
// It specifies the attachment to be deleted by its ID.
type DeleteIssueAttachmentParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number containing the attachment.
Index int `json:"index"`
// AttachmentID is the unique identifier of the attachment to delete.
AttachmentID string `json:"attachment_id"`
}
// DeleteIssueAttachmentImpl implements the destructive MCP tool for deleting an issue attachment.
// This is an idempotent but irreversible operation. Note: This feature is not supported
// by the official Forgejo SDK and requires a custom HTTP implementation.
type DeleteIssueAttachmentImpl struct {
Client *tools.Client
}
// Definition describes the `delete_issue_attachment` tool. It requires the issue `index`
// and `attachment_id`. It is marked as a destructive operation.
func (DeleteIssueAttachmentImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "delete_issue_attachment",
Title: "Delete Issue Attachment",
Description: "Delete a specific attachment from an issue.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(true),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"attachment_id": {
Type: "string",
Description: "Attachment ID to delete",
},
},
Required: []string{"owner", "repo", "index", "attachment_id"},
},
}
}
// Handler implements the logic for deleting an issue attachment. It performs a custom
// HTTP DELETE request to the `/repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}`
// endpoint. On success, it returns a simple text confirmation.
func (impl DeleteIssueAttachmentImpl) Handler() mcp.ToolHandlerFor[DeleteIssueAttachmentParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteIssueAttachmentParams) (*mcp.CallToolResult, any, error) {
p := args
// Convert attachment ID from string to int64
attachmentID, err := strconv.ParseInt(p.AttachmentID, 10, 64)
if err != nil {
return nil, nil, fmt.Errorf("invalid attachment ID: %w", err)
}
// Delete the attachment using the custom client method
err = impl.Client.MyDeleteIssueAttachment(p.Owner, p.Repo, int64(p.Index), attachmentID)
if err != nil {
return nil, nil, fmt.Errorf("failed to delete issue attachment: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("Issue attachment %s deleted successfully from issue #%d", p.AttachmentID, p.Index),
},
},
}, nil, nil
}
}
// EditIssueAttachmentParams defines the parameters for editing an issue attachment.
// It specifies the attachment to edit and its new name.
type EditIssueAttachmentParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number containing the attachment.
Index int `json:"index"`
// AttachmentID is the unique identifier of the attachment to edit.
AttachmentID string `json:"attachment_id"`
// Name is the new display name for the attachment.
Name string `json:"name"`
}
// EditIssueAttachmentImpl implements the MCP tool for editing an issue attachment.
// This is an idempotent operation. Note: This feature is not supported by the
// official Forgejo SDK and requires a custom HTTP implementation.
type EditIssueAttachmentImpl struct {
Client *tools.Client
}
// Definition describes the `edit_issue_attachment` tool. It requires the issue `index`,
// `attachment_id`, and a new `name`. It is marked as idempotent.
func (EditIssueAttachmentImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "edit_issue_attachment",
Title: "Edit Issue Attachment",
Description: "Edit an attachment's metadata such as display name.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"attachment_id": {
Type: "string",
Description: "Attachment ID to edit",
},
"name": {
Type: "string",
Description: "New display name for the attachment",
},
},
Required: []string{"owner", "repo", "index", "attachment_id", "name"},
},
}
}
// Handler implements the logic for editing an issue attachment. It performs a custom
// HTTP PATCH request to the `/repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}`
// endpoint. It will return an error if the attachment is not found.
func (impl EditIssueAttachmentImpl) Handler() mcp.ToolHandlerFor[EditIssueAttachmentParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args EditIssueAttachmentParams) (*mcp.CallToolResult, any, error) {
p := args
// Convert attachment ID from string to int64
attachmentID, err := strconv.ParseInt(p.AttachmentID, 10, 64)
if err != nil {
return nil, nil, fmt.Errorf("invalid attachment ID: %w", err)
}
// Create options struct from parameters
options := tools.MyEditAttachmentOptions{
Name: p.Name,
}
// Edit the attachment using the custom client method
attachment, err := impl.Client.MyEditIssueAttachment(p.Owner, p.Repo, int64(p.Index), attachmentID, options)
if err != nil {
return nil, nil, fmt.Errorf("failed to edit issue attachment: %w", err)
}
// Convert to types.Attachment for consistent formatting
result := &types.Attachment{Attachment: attachment}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("# Issue Attachment Updated\n\n%s", result.ToMarkdown()),
},
},
}, nil, nil
}
}
```