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