#
tokens: 49633/50000 74/87 files (page 1/3)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 1/3FirstPrevNextLast