This is page 2 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
--------------------------------------------------------------------------------
/tools/issue/label.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 issue
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 | // AddIssueLabelsParams defines the parameters for the add_issue_labels tool.
22 | // It specifies the issue and the label IDs to be added.
23 | type AddIssueLabelsParams 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 | // Index is the issue number.
29 | Index int `json:"index"`
30 | // Labels is a slice of label IDs to add to the issue.
31 | Labels []int `json:"labels"`
32 | }
33 |
34 | // AddIssueLabelsImpl implements the MCP tool for adding labels to an issue.
35 | // This is an idempotent operation that uses the Forgejo SDK to associate one
36 | // or more existing labels with an issue.
37 | type AddIssueLabelsImpl struct {
38 | Client *tools.Client
39 | }
40 |
41 | // Definition describes the `add_issue_labels` tool. It requires the issue's `index`
42 | // and an array of `labels` (IDs). It is marked as idempotent.
43 | func (AddIssueLabelsImpl) Definition() *mcp.Tool {
44 | return &mcp.Tool{
45 | Name: "add_issue_labels",
46 | Title: "Add Issue Labels",
47 | Description: "Add labels to an existing issue.",
48 | Annotations: &mcp.ToolAnnotations{
49 | ReadOnlyHint: false,
50 | DestructiveHint: tools.BoolPtr(false),
51 | IdempotentHint: true,
52 | },
53 | InputSchema: &jsonschema.Schema{
54 | Type: "object",
55 | Properties: map[string]*jsonschema.Schema{
56 | "owner": {
57 | Type: "string",
58 | Description: "Repository owner (username or organization name)",
59 | },
60 | "repo": {
61 | Type: "string",
62 | Description: "Repository name",
63 | },
64 | "index": {
65 | Type: "integer",
66 | Description: "Issue index number",
67 | },
68 | "labels": {
69 | Type: "array",
70 | Items: &jsonschema.Schema{
71 | Type: "integer",
72 | },
73 | Description: "Array of label IDs to add to this issue",
74 | MinItems: tools.IntPtr(1),
75 | },
76 | },
77 | Required: []string{"owner", "repo", "index", "labels"},
78 | },
79 | }
80 | }
81 |
82 | // Handler implements the logic for adding labels to an issue. It calls the
83 | // Forgejo SDK's `AddIssueLabels` function. It will return an error if the issue
84 | // or any of the label IDs are not found.
85 | func (impl AddIssueLabelsImpl) Handler() mcp.ToolHandlerFor[AddIssueLabelsParams, any] {
86 | return func(ctx context.Context, req *mcp.CallToolRequest, args AddIssueLabelsParams) (*mcp.CallToolResult, any, error) {
87 | p := args
88 |
89 | // Convert int labels to int64
90 | labelIDs := make([]int64, len(p.Labels))
91 | for i, label := range p.Labels {
92 | labelIDs[i] = int64(label)
93 | }
94 |
95 | opt := forgejo.IssueLabelsOption{
96 | Labels: labelIDs,
97 | }
98 |
99 | labels, _, err := impl.Client.AddIssueLabels(p.Owner, p.Repo, int64(p.Index), opt)
100 | if err != nil {
101 | return nil, nil, fmt.Errorf("failed to add labels: %w", err)
102 | }
103 |
104 | // Convert to our types
105 | var labelsMarkdown string
106 | for _, label := range labels {
107 | labelWrapper := &types.Label{Label: label}
108 | labelsMarkdown += labelWrapper.ToMarkdown() + "\n"
109 | }
110 |
111 | content := fmt.Sprintf("Added %d labels to issue #%d\n\n%s", len(labels), p.Index, labelsMarkdown)
112 |
113 | return &mcp.CallToolResult{
114 | Content: []mcp.Content{
115 | &mcp.TextContent{
116 | Text: content,
117 | },
118 | },
119 | }, nil, nil
120 | }
121 | }
122 |
123 | // RemoveIssueLabelParams defines the parameters for the remove_issue_label tool.
124 | // It specifies the issue and the single label ID to be removed.
125 | type RemoveIssueLabelParams struct {
126 | // Owner is the username or organization name that owns the repository.
127 | Owner string `json:"owner"`
128 | // Repo is the name of the repository.
129 | Repo string `json:"repo"`
130 | // Index is the issue number.
131 | Index int `json:"index"`
132 | // Label is the ID of the label to remove from the issue.
133 | Label int `json:"label"`
134 | }
135 |
136 | // RemoveIssueLabelImpl implements the MCP tool for removing a label from an issue.
137 | // This is an idempotent operation that uses the Forgejo SDK to disassociate a
138 | // label from an issue.
139 | type RemoveIssueLabelImpl struct {
140 | Client *tools.Client
141 | }
142 |
143 | // Definition describes the `remove_issue_label` tool. It requires the issue's
144 | // `index` and a single `label` ID to remove. It is marked as idempotent.
145 | func (RemoveIssueLabelImpl) Definition() *mcp.Tool {
146 | return &mcp.Tool{
147 | Name: "remove_issue_label",
148 | Title: "Remove Issue Label",
149 | Description: "Remove a specific label from an issue.",
150 | Annotations: &mcp.ToolAnnotations{
151 | ReadOnlyHint: false,
152 | DestructiveHint: tools.BoolPtr(false),
153 | IdempotentHint: true,
154 | },
155 | InputSchema: &jsonschema.Schema{
156 | Type: "object",
157 | Properties: map[string]*jsonschema.Schema{
158 | "owner": {
159 | Type: "string",
160 | Description: "Repository owner (username or organization name)",
161 | },
162 | "repo": {
163 | Type: "string",
164 | Description: "Repository name",
165 | },
166 | "index": {
167 | Type: "integer",
168 | Description: "Issue index number",
169 | },
170 | "label": {
171 | Type: "integer",
172 | Description: "Label ID to remove from this issue",
173 | },
174 | },
175 | Required: []string{"owner", "repo", "index", "label"},
176 | },
177 | }
178 | }
179 |
180 | // Handler implements the logic for removing a label from an issue. It calls the
181 | // Forgejo SDK's `DeleteIssueLabel` function. On success, it returns a simple
182 | // text confirmation. It will return an error if the issue or label is not found.
183 | func (impl RemoveIssueLabelImpl) Handler() mcp.ToolHandlerFor[RemoveIssueLabelParams, any] {
184 | return func(ctx context.Context, req *mcp.CallToolRequest, args RemoveIssueLabelParams) (*mcp.CallToolResult, any, error) {
185 | p := args
186 |
187 | _, err := impl.Client.DeleteIssueLabel(p.Owner, p.Repo, int64(p.Index), int64(p.Label))
188 | if err != nil {
189 | return nil, nil, fmt.Errorf("failed to remove label: %w", err)
190 | }
191 |
192 | return &mcp.CallToolResult{
193 | Content: []mcp.Content{
194 | &mcp.TextContent{
195 | Text: fmt.Sprintf("Label %d successfully removed from issue #%d.", p.Label, p.Index),
196 | },
197 | },
198 | }, nil, nil
199 | }
200 | }
201 |
202 | // ReplaceIssueLabelsParams defines the parameters for the replace_issue_labels tool.
203 | // It specifies the issue and the new set of label IDs.
204 | type ReplaceIssueLabelsParams struct {
205 | // Owner is the username or organization name that owns the repository.
206 | Owner string `json:"owner"`
207 | // Repo is the name of the repository.
208 | Repo string `json:"repo"`
209 | // Index is the issue number.
210 | Index int `json:"index"`
211 | // Labels is a slice of label IDs that will replace all existing labels on the issue.
212 | Labels []int `json:"labels"`
213 | }
214 |
215 | // ReplaceIssueLabelsImpl implements the MCP tool for replacing all labels on an issue.
216 | // This is an idempotent operation that uses the Forgejo SDK to set the definitive
217 | // list of labels for an issue.
218 | type ReplaceIssueLabelsImpl struct {
219 | Client *tools.Client
220 | }
221 |
222 | // Definition describes the `replace_issue_labels` tool. It requires the issue's
223 | // `index` and an array of `labels` (IDs) to apply. It is marked as idempotent.
224 | func (ReplaceIssueLabelsImpl) Definition() *mcp.Tool {
225 | return &mcp.Tool{
226 | Name: "replace_issue_labels",
227 | Title: "Replace Issue Labels",
228 | Description: "Replace all labels on an issue with a new set of labels.",
229 | Annotations: &mcp.ToolAnnotations{
230 | ReadOnlyHint: false,
231 | DestructiveHint: tools.BoolPtr(false),
232 | IdempotentHint: true,
233 | },
234 | InputSchema: &jsonschema.Schema{
235 | Type: "object",
236 | Properties: map[string]*jsonschema.Schema{
237 | "owner": {
238 | Type: "string",
239 | Description: "Repository owner (username or organization name)",
240 | },
241 | "repo": {
242 | Type: "string",
243 | Description: "Repository name",
244 | },
245 | "index": {
246 | Type: "integer",
247 | Description: "Issue index number",
248 | },
249 | "labels": {
250 | Type: "array",
251 | Items: &jsonschema.Schema{
252 | Type: "integer",
253 | },
254 | Description: "Array of label IDs to set on this issue (replaces all existing labels)",
255 | },
256 | },
257 | Required: []string{"owner", "repo", "index", "labels"},
258 | },
259 | }
260 | }
261 |
262 | // Handler implements the logic for replacing issue labels. It calls the Forgejo
263 | // SDK's `ReplaceIssueLabels` function. It will return an error if the issue or
264 | // any of the label IDs are not found.
265 | func (impl ReplaceIssueLabelsImpl) Handler() mcp.ToolHandlerFor[ReplaceIssueLabelsParams, any] {
266 | return func(ctx context.Context, req *mcp.CallToolRequest, args ReplaceIssueLabelsParams) (*mcp.CallToolResult, any, error) {
267 | p := args
268 |
269 | // Convert int labels to int64
270 | labelIDs := make([]int64, len(p.Labels))
271 | for i, label := range p.Labels {
272 | labelIDs[i] = int64(label)
273 | }
274 |
275 | opt := forgejo.IssueLabelsOption{
276 | Labels: labelIDs,
277 | }
278 |
279 | labels, _, err := impl.Client.ReplaceIssueLabels(p.Owner, p.Repo, int64(p.Index), opt)
280 | if err != nil {
281 | return nil, nil, fmt.Errorf("failed to replace labels: %w", err)
282 | }
283 |
284 | // Convert to our types
285 | var labelsMarkdown string
286 | for _, label := range labels {
287 | labelWrapper := &types.Label{Label: label}
288 | labelsMarkdown += labelWrapper.ToMarkdown() + "\n"
289 | }
290 |
291 | content := fmt.Sprintf("Replaced labels for issue #%d with %d labels\n\n%s", p.Index, len(labels), labelsMarkdown)
292 |
293 | return &mcp.CallToolResult{
294 | Content: []mcp.Content{
295 | &mcp.TextContent{
296 | Text: content,
297 | },
298 | },
299 | }, nil, nil
300 | }
301 | }
302 |
```
--------------------------------------------------------------------------------
/features.md:
--------------------------------------------------------------------------------
```markdown
1 | # Feature List
2 |
3 | ## Implementation Strategy
4 |
5 | - 🟢 **SDK**: Implementation using official SDK
6 | - 🟡 **Custom**: Custom HTTP request implementation (reusing SDK authentication)
7 | - 🔴 **Mixed**: Some features using SDK, some requiring custom implementation
8 |
9 | ### Label Features
10 |
11 | Labels available for a specific repository
12 |
13 | - **List Labels** 🟢
14 | - `GET /repos/{owner}/{repo}/labels`
15 | - SDK: `ListRepoLabels(owner, repo string, opt ListLabelsOptions) ([]*Label, *Response, error)`
16 | - **Modify label name, description, and color** 🟢
17 | - `PATCH /repos/{owner}/{repo}/labels/{id}`
18 | - SDK: `EditLabel(owner, repo string, id int64, opt EditLabelOption) (*Label, *Response, error)`
19 | - **Create or delete labels** 🟢
20 | - `POST /repos/{owner}/{repo}/labels`
21 | - SDK: `CreateLabel(owner, repo string, opt CreateLabelOption) (*Label, *Response, error)`
22 | - `DELETE /repos/{owner}/{repo}/labels/{id}`
23 | - SDK: `DeleteLabel(owner, repo string, id int64) (*Response, error)`
24 |
25 | ### Milestone Features 🟢
26 |
27 | - **List Milestones**
28 | - `GET /repos/{owner}/{repo}/milestones`
29 | - SDK: `ListRepoMilestones(owner, repo string, opt ListMilestoneOption) ([]*Milestone, *Response, error)`
30 | - **Create, delete, and modify milestones (including title, due date, and description)**
31 | - `POST /repos/{owner}/{repo}/milestones`
32 | - SDK: `CreateMilestone(owner, repo string, opt CreateMilestoneOption) (*Milestone, *Response, error)`
33 | - `DELETE /repos/{owner}/{repo}/milestones/{id}`
34 | - SDK: `DeleteMilestone(owner, repo string, id int64) (*Response, error)`
35 | - `PATCH /repos/{owner}/{repo}/milestones/{id}`
36 | - SDK: `EditMilestone(owner, repo string, id int64, opt EditMilestoneOption) (*Milestone, *Response, error)`
37 |
38 | ### Issue Features 🔴
39 |
40 | - **List Repository Issues** 🟢
41 | - `GET /repos/{owner}/{repo}/issues`
42 | - SDK: `ListRepoIssues(owner, repo string, opt ListIssueOption) ([]*Issue, *Response, error)`
43 | - Supports filters: state, labels, milestones, assignees, search, date filters
44 | - **Get Specific Issue Details** 🟢
45 | - `GET /repos/{owner}/{repo}/issues/{index}`
46 | - SDK: `GetIssue(owner, repo string, index int64) (*Issue, *Response, error)`
47 | - **List Issue Comments** 🟢
48 | - `GET /repos/{owner}/{repo}/issues/{index}/comments`
49 | - SDK: `ListIssueComments(owner, repo string, index int64, opt ListIssueCommentOptions) ([]*Comment, *Response, error)`
50 | - **Create new issue** 🟢
51 | - `POST /repos/{owner}/{repo}/issues`
52 | - SDK: `CreateIssue(owner, repo string, opt CreateIssueOption) (*Issue, *Response, error)`
53 | - **Comment on existing issue** 🟢
54 | - `POST /repos/{owner}/{repo}/issues/{index}/comments`
55 | - SDK: `CreateIssueComment(owner, repo string, index int64, opt CreateIssueCommentOption) (*Comment, *Response, error)`
56 | - **Close issue** 🟢
57 | - `PATCH /repos/{owner}/{repo}/issues/{index}` (set `state` to `closed`)
58 | - SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
59 | - **Modify issue data** 🟢
60 | - **Description:** `PATCH /repos/{owner}/{repo}/issues/{index}` (modify `body`)
61 | - SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
62 | - **Labels:** 🟢
63 | - `POST /repos/{owner}/{repo}/issues/{index}/labels` (add)
64 | - SDK: `AddIssueLabels(owner, repo string, index int64, opt IssueLabelsOption) ([]*Label, *Response, error)`
65 | - `DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id}` (remove)
66 | - SDK: `DeleteIssueLabel(owner, repo string, index, label int64) (*Response, error)`
67 | - `PUT /repos/{owner}/{repo}/issues/{index}/labels` (replace)
68 | - SDK: `ReplaceIssueLabels(owner, repo string, index int64, opt IssueLabelsOption) ([]*Label, *Response, error)`
69 | - **Assignees:** 🟢 `PATCH /repos/{owner}/{repo}/issues/{index}` (modify `assignees`)
70 | - SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
71 | - **Milestone:** 🟢 `PATCH /repos/{owner}/{repo}/issues/{index}` (modify `milestone`)
72 | - SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
73 | - **Due date:** 🟢 `PATCH /repos/{owner}/{repo}/issues/{index}` (modify `due_date`)
74 | - SDK: `EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, *Response, error)`
75 | - **Dependency management:** 🟡
76 | - **Dependencies (issues that block this issue):**
77 | - **Add dependency:** `POST /repos/{owner}/{repo}/issues/{index}/dependencies`
78 | - Custom: Not supported by SDK, requires custom HTTP request
79 | - **List dependencies:** `GET /repos/{owner}/{repo}/issues/{index}/dependencies`
80 | - Custom: Not supported by SDK, requires custom HTTP request
81 | - **Remove dependency:** `DELETE /repos/{owner}/{repo}/issues/{index}/dependencies` (via request body)
82 | - Custom: Not supported by SDK, requires custom HTTP request
83 | - **Blocking (issues blocked by this issue):**
84 | - **Add blocking:** `POST /repos/{owner}/{repo}/issues/{index}/blocks`
85 | - Custom: Not supported by SDK, requires custom HTTP request
86 | - **List blocking:** `GET /repos/{owner}/{repo}/issues/{index}/blocks`
87 | - Custom: Not supported by SDK, requires custom HTTP request
88 | - **Remove blocking:** `DELETE /repos/{owner}/{repo}/issues/{index}/blocks` (via request body)
89 | - Custom: Not supported by SDK, requires custom HTTP request
90 | - **Edit Issue Comments** 🟢
91 | - `PATCH /repos/{owner}/{repo}/issues/comments/{id}`
92 | - SDK: `EditIssueComment(owner, repo string, commentID int64, opt EditIssueCommentOption) (*Comment, *Response, error)`
93 | - **Delete Issue Comments** 🟢
94 | - `DELETE /repos/{owner}/{repo}/issues/comments/{id}`
95 | - SDK: `DeleteIssueComment(owner, repo string, commentID int64) (*Response, error)`
96 | - **Attachment management** 🟡
97 | - **List attachments:** `GET /repos/{owner}/{repo}/issues/{index}/assets`
98 | - Custom: Not supported by SDK, requires custom HTTP request
99 | - **Delete attachment:** `DELETE /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}`
100 | - Custom: Not supported by SDK, requires custom HTTP request
101 | - **Modify attachment:** `PATCH /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}`
102 | - Custom: Not supported by SDK, requires custom HTTP request
103 |
104 | ### Wiki Features 🟡
105 |
106 | - **Query pages**
107 | - `GET /repos/{owner}/{repo}/wiki/page/{pageName}`
108 | - Custom: Not supported by SDK, requires custom HTTP request
109 | - **List pages**
110 | - `GET /repos/{owner}/{repo}/wiki/pages`
111 | - Custom: Not supported by SDK, requires custom HTTP request
112 | - **Create, delete, and modify pages**
113 | - `POST /repos/{owner}/{repo}/wiki/new`
114 | - Custom: Not supported by SDK, requires custom HTTP request
115 | - `DELETE /repos/{owner}/{repo}/wiki/page/{pageName}`
116 | - Custom: Not supported by SDK, requires custom HTTP request
117 | - `PATCH /repos/{owner}/{repo}/wiki/page/{pageName}`
118 | - Custom: Not supported by SDK, requires custom HTTP request
119 |
120 | ### Release Management 🟢
121 |
122 | - **List Releases**
123 | - `GET /repos/{owner}/{repo}/releases`
124 | - SDK: `ListReleases(owner, repo string, opt ListReleasesOptions) ([]*Release, *Response, error)`
125 | - **Create, delete, and modify releases**
126 | - `POST /repos/{owner}/{repo}/releases`
127 | - SDK: `CreateRelease(owner, repo string, opt CreateReleaseOption) (*Release, *Response, error)`
128 | - `DELETE /repos/{owner}/{repo}/releases/{id}`
129 | - SDK: `DeleteRelease(user, repo string, id int64) (*Response, error)`
130 | - `PATCH /repos/{owner}/{repo}/releases/{id}`
131 | - SDK: `EditRelease(owner, repo string, id int64, form EditReleaseOption) (*Release, *Response, error)`
132 | - **Attachment management**
133 | - **List attachments:** `GET /repos/{owner}/{repo}/releases/{id}/assets`
134 | - SDK: `ListReleaseAttachments(user, repo string, release int64, opt ListReleaseAttachmentsOptions) ([]*Attachment, *Response, error)`
135 | - **Delete attachment:** `DELETE /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}`
136 | - SDK: `DeleteReleaseAttachment(user, repo string, release, id int64) (*Response, error)`
137 | - **Modify attachment:** `PATCH /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}`
138 | - SDK: `EditReleaseAttachment(user, repo string, release, attachment int64, form EditAttachmentOptions) (*Attachment, *Response, error)`
139 |
140 | ### PR Management 🟢
141 |
142 | - **List and query PRs**
143 | - `GET /repos/{owner}/{repo}/pulls`
144 | - SDK: `ListRepoPullRequests(owner, repo string, opt ListPullRequestsOptions) ([]*PullRequest, *Response, error)`
145 | - `GET /repos/{owner}/{repo}/pulls/{index}`
146 | - SDK: `GetPullRequest(owner, repo string, index int64) (*PullRequest, *Response, error)`
147 |
148 | ### Repository Management 🟢
149 |
150 | - **List and query repositories**
151 | - `GET /repos/search`
152 | - SDK: `SearchRepos(opt SearchRepoOptions) ([]*Repository, *Response, error)`
153 | - `GET /user/repos`
154 | - SDK: `ListMyRepos(opt ListReposOptions) ([]*Repository, *Response, error)`
155 | - `GET /orgs/{org}/repos`
156 | - SDK: `ListOrgRepos(org string, opt ListOrgReposOptions) ([]*Repository, *Response, error)`
157 | - **Get Specific Repository Information** 🟢
158 | - `GET /repos/{owner}/{repo}`
159 | - SDK: `GetRepo(owner, repo string) (*Repository, *Response, error)`
160 |
161 | ### Forgejo Actions (CI/CD) 🟡
162 |
163 | - **List Action execution tasks**
164 | - `GET /repos/{owner}/{repo}/actions/tasks`
165 | - Custom: Not supported by SDK, requires custom HTTP request
166 |
167 | ## Summary
168 |
169 | - 🟢 **Fully supported (5/7)**: Label, Milestone, Release, PR, Repository management
170 | - 🔴 **Partially supported (1/7)**: Issue features (attachments and dependencies require custom implementation)
171 | - 🟡 **Requires custom implementation (2/7)**: Wiki, Forgejo Actions
172 |
173 | **Recommended Hybrid approach**: Approximately 71% of features can use SDK, remaining features require custom HTTP requests.
174 |
```
--------------------------------------------------------------------------------
/tools/release/attach.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 release
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 | // ListReleaseAttachmentsParams defines the parameters for the list_release_attachments tool.
22 | // It specifies the release to list attachments from.
23 | type ListReleaseAttachmentsParams 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 | // ReleaseID is the unique identifier of the release.
29 | ReleaseID int `json:"release_id"`
30 | }
31 |
32 | // ListReleaseAttachmentsImpl implements the read-only MCP tool for listing release attachments.
33 | // This is a safe, idempotent operation that uses the Forgejo SDK to fetch a list
34 | // of all attachments for a specific release.
35 | type ListReleaseAttachmentsImpl struct {
36 | Client *tools.Client
37 | }
38 |
39 | // Definition describes the `list_release_attachments` tool. It requires `owner`,
40 | // `repo`, and `release_id`. It is marked as a safe, read-only operation.
41 | func (ListReleaseAttachmentsImpl) Definition() *mcp.Tool {
42 | return &mcp.Tool{
43 | Name: "list_release_attachments",
44 | Title: "List Release Attachments",
45 | Description: "List all attachments for a specific release.",
46 | Annotations: &mcp.ToolAnnotations{
47 | ReadOnlyHint: true,
48 | IdempotentHint: true,
49 | },
50 | InputSchema: &jsonschema.Schema{
51 | Type: "object",
52 | Properties: map[string]*jsonschema.Schema{
53 | "owner": {
54 | Type: "string",
55 | Description: "Repository owner (username or organization name)",
56 | },
57 | "repo": {
58 | Type: "string",
59 | Description: "Repository name",
60 | },
61 | "release_id": {
62 | Type: "integer",
63 | Description: "Release ID",
64 | },
65 | },
66 | Required: []string{"owner", "repo", "release_id"},
67 | },
68 | }
69 | }
70 |
71 | // Handler implements the logic for listing release attachments. It calls the Forgejo
72 | // SDK's `ListReleaseAttachments` function and formats the results into a markdown list.
73 | func (impl ListReleaseAttachmentsImpl) Handler() mcp.ToolHandlerFor[ListReleaseAttachmentsParams, any] {
74 | return func(ctx context.Context, req *mcp.CallToolRequest, args ListReleaseAttachmentsParams) (*mcp.CallToolResult, any, error) {
75 | p := args
76 |
77 | // Call SDK
78 | attachments, _, err := impl.Client.ListReleaseAttachments(p.Owner, p.Repo, int64(p.ReleaseID), forgejo.ListReleaseAttachmentsOptions{})
79 | if err != nil {
80 | return nil, nil, fmt.Errorf("failed to list release attachments: %w", err)
81 | }
82 |
83 | // Convert to our types and format
84 | var content string
85 | if len(attachments) == 0 {
86 | content = "No attachments found for this release."
87 | } else {
88 | // Convert attachments to our type
89 | attachmentList := make(types.AttachmentList, len(attachments))
90 | for i, attachment := range attachments {
91 | attachmentList[i] = &types.Attachment{Attachment: attachment}
92 | }
93 |
94 | content = fmt.Sprintf("Found %d attachments\n\n%s",
95 | len(attachments), attachmentList.ToMarkdown())
96 | }
97 |
98 | return &mcp.CallToolResult{
99 | Content: []mcp.Content{
100 | &mcp.TextContent{
101 | Text: content,
102 | },
103 | },
104 | }, nil, nil
105 | }
106 | }
107 |
108 | // EditReleaseAttachmentParams defines the parameters for editing a release attachment.
109 | // It specifies the attachment to edit and its new name.
110 | type EditReleaseAttachmentParams struct {
111 | // Owner is the username or organization name that owns the repository.
112 | Owner string `json:"owner"`
113 | // Repo is the name of the repository.
114 | Repo string `json:"repo"`
115 | // ReleaseID is the unique identifier of the release containing the attachment.
116 | ReleaseID int `json:"release_id"`
117 | // AttachmentID is the unique identifier of the attachment to edit.
118 | AttachmentID int `json:"attachment_id"`
119 | // Name is the new display name for the attachment.
120 | Name string `json:"name"`
121 | }
122 |
123 | // EditReleaseAttachmentImpl implements the MCP tool for editing a release attachment.
124 | // This is an idempotent operation that renames an existing attachment using the
125 | // Forgejo SDK.
126 | type EditReleaseAttachmentImpl struct {
127 | Client *tools.Client
128 | }
129 |
130 | // Definition describes the `edit_release_attachment` tool. It requires `release_id`,
131 | // `attachment_id`, and a new `name`. It is marked as idempotent.
132 | func (EditReleaseAttachmentImpl) Definition() *mcp.Tool {
133 | return &mcp.Tool{
134 | Name: "edit_release_attachment",
135 | Title: "Edit Release Attachment",
136 | Description: "Edit a release attachment's metadata (like display name).",
137 | Annotations: &mcp.ToolAnnotations{
138 | ReadOnlyHint: false,
139 | DestructiveHint: tools.BoolPtr(false),
140 | IdempotentHint: true,
141 | },
142 | InputSchema: &jsonschema.Schema{
143 | Type: "object",
144 | Properties: map[string]*jsonschema.Schema{
145 | "owner": {
146 | Type: "string",
147 | Description: "Repository owner (username or organization name)",
148 | },
149 | "repo": {
150 | Type: "string",
151 | Description: "Repository name",
152 | },
153 | "release_id": {
154 | Type: "integer",
155 | Description: "Release ID",
156 | },
157 | "attachment_id": {
158 | Type: "integer",
159 | Description: "Attachment ID to edit",
160 | },
161 | "name": {
162 | Type: "string",
163 | Description: "New display name for the attachment",
164 | },
165 | },
166 | Required: []string{"owner", "repo", "release_id", "attachment_id", "name"},
167 | },
168 | }
169 | }
170 |
171 | // Handler implements the logic for editing a release attachment. It calls the Forgejo
172 | // SDK's `EditReleaseAttachment` function. It will return an error if the attachment
173 | // ID is not found.
174 | func (impl EditReleaseAttachmentImpl) Handler() mcp.ToolHandlerFor[EditReleaseAttachmentParams, any] {
175 | return func(ctx context.Context, req *mcp.CallToolRequest, args EditReleaseAttachmentParams) (*mcp.CallToolResult, any, error) {
176 | p := args
177 |
178 | // Build options for SDK call
179 | opt := forgejo.EditAttachmentOptions{
180 | Name: p.Name,
181 | }
182 |
183 | // Call SDK
184 | attachment, _, err := impl.Client.EditReleaseAttachment(p.Owner, p.Repo, int64(p.ReleaseID), int64(p.AttachmentID), opt)
185 | if err != nil {
186 | return nil, nil, fmt.Errorf("failed to edit release attachment: %w", err)
187 | }
188 |
189 | // Convert to our type and format
190 | attachmentWrapper := &types.Attachment{Attachment: attachment}
191 |
192 | return &mcp.CallToolResult{
193 | Content: []mcp.Content{
194 | &mcp.TextContent{
195 | Text: attachmentWrapper.ToMarkdown(),
196 | },
197 | },
198 | }, nil, nil
199 | }
200 | }
201 |
202 | // DeleteReleaseAttachmentParams defines the parameters for deleting a release attachment.
203 | // It specifies the attachment to be deleted by its ID.
204 | type DeleteReleaseAttachmentParams struct {
205 | // Owner is the username or organization name that owns the repository.
206 | Owner string `json:"owner"`
207 | // Repo is the name of the repository.
208 | Repo string `json:"repo"`
209 | // ReleaseID is the unique identifier of the release containing the attachment.
210 | ReleaseID int `json:"release_id"`
211 | // AttachmentID is the unique identifier of the attachment to delete.
212 | AttachmentID int `json:"attachment_id"`
213 | }
214 |
215 | // DeleteReleaseAttachmentImpl implements the destructive MCP tool for deleting a release attachment.
216 | // This is an idempotent but irreversible operation that removes an attachment from a
217 | // release using the Forgejo SDK.
218 | type DeleteReleaseAttachmentImpl struct {
219 | Client *tools.Client
220 | }
221 |
222 | // Definition describes the `delete_release_attachment` tool. It requires `release_id`
223 | // and `attachment_id`. It is marked as a destructive operation to ensure clients
224 | // can warn the user before execution.
225 | func (DeleteReleaseAttachmentImpl) Definition() *mcp.Tool {
226 | return &mcp.Tool{
227 | Name: "delete_release_attachment",
228 | Title: "Delete Release Attachment",
229 | Description: "Delete an attachment from a release.",
230 | Annotations: &mcp.ToolAnnotations{
231 | ReadOnlyHint: false,
232 | DestructiveHint: tools.BoolPtr(true),
233 | IdempotentHint: true,
234 | },
235 | InputSchema: &jsonschema.Schema{
236 | Type: "object",
237 | Properties: map[string]*jsonschema.Schema{
238 | "owner": {
239 | Type: "string",
240 | Description: "Repository owner (username or organization name)",
241 | },
242 | "repo": {
243 | Type: "string",
244 | Description: "Repository name",
245 | },
246 | "release_id": {
247 | Type: "integer",
248 | Description: "Release ID",
249 | },
250 | "attachment_id": {
251 | Type: "integer",
252 | Description: "Attachment ID to delete",
253 | },
254 | },
255 | Required: []string{"owner", "repo", "release_id", "attachment_id"},
256 | },
257 | }
258 | }
259 |
260 | // Handler implements the logic for deleting a release attachment. It calls the Forgejo
261 | // SDK's `DeleteReleaseAttachment` function. On success, it returns a simple text
262 | // confirmation. It will return an error if the attachment does not exist.
263 | func (impl DeleteReleaseAttachmentImpl) Handler() mcp.ToolHandlerFor[DeleteReleaseAttachmentParams, any] {
264 | return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteReleaseAttachmentParams) (*mcp.CallToolResult, any, error) {
265 | p := args
266 |
267 | // Call SDK
268 | _, err := impl.Client.DeleteReleaseAttachment(p.Owner, p.Repo, int64(p.ReleaseID), int64(p.AttachmentID))
269 | if err != nil {
270 | return nil, nil, fmt.Errorf("failed to delete release attachment: %w", err)
271 | }
272 |
273 | // Return success message
274 | emptyResponse := types.EmptyResponse{}
275 |
276 | return &mcp.CallToolResult{
277 | Content: []mcp.Content{
278 | &mcp.TextContent{
279 | Text: emptyResponse.ToMarkdown(),
280 | },
281 | },
282 | }, nil, nil
283 | }
284 | }
285 |
```
--------------------------------------------------------------------------------
/tools/issue/attach.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 issue
8 |
9 | import (
10 | "context"
11 | "fmt"
12 | "strconv"
13 |
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 | // ListIssueAttachmentsParams defines the parameters for the list_issue_attachments tool.
22 | // It specifies the issue from which to list attachments.
23 | type ListIssueAttachmentsParams 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 | // Index is the issue number.
29 | Index int `json:"index"`
30 | }
31 |
32 | // ListIssueAttachmentsImpl implements the read-only MCP tool for listing issue attachments.
33 | // This is a safe, idempotent operation. Note: This feature is not supported by the
34 | // official Forgejo SDK and requires a custom HTTP implementation.
35 | type ListIssueAttachmentsImpl struct {
36 | Client *tools.Client
37 | }
38 |
39 | // Definition describes the `list_issue_attachments` tool. It requires `owner`, `repo`,
40 | // and the issue `index`. It is marked as a safe, read-only operation.
41 | func (ListIssueAttachmentsImpl) Definition() *mcp.Tool {
42 | return &mcp.Tool{
43 | Name: "list_issue_attachments",
44 | Title: "List Issue Attachments",
45 | Description: "List all attachments on an issue. Returns attachment information including names, sizes, and download URLs.",
46 | Annotations: &mcp.ToolAnnotations{
47 | ReadOnlyHint: true,
48 | IdempotentHint: true,
49 | },
50 | InputSchema: &jsonschema.Schema{
51 | Type: "object",
52 | Properties: map[string]*jsonschema.Schema{
53 | "owner": {
54 | Type: "string",
55 | Description: "Repository owner (username or organization name)",
56 | },
57 | "repo": {
58 | Type: "string",
59 | Description: "Repository name",
60 | },
61 | "index": {
62 | Type: "integer",
63 | Description: "Issue index number",
64 | },
65 | },
66 | Required: []string{"owner", "repo", "index"},
67 | },
68 | }
69 | }
70 |
71 | // Handler implements the logic for listing issue attachments. It performs a custom
72 | // HTTP GET request to the `/repos/{owner}/{repo}/issues/{index}/assets`
73 | // endpoint and formats the results into a markdown list.
74 | func (impl ListIssueAttachmentsImpl) Handler() mcp.ToolHandlerFor[ListIssueAttachmentsParams, any] {
75 | return func(ctx context.Context, req *mcp.CallToolRequest, args ListIssueAttachmentsParams) (*mcp.CallToolResult, any, error) {
76 | p := args
77 |
78 | // List issue attachments using the custom client method
79 | attachments, err := impl.Client.MyListIssueAttachments(p.Owner, p.Repo, int64(p.Index))
80 | if err != nil {
81 | return nil, nil, fmt.Errorf("failed to list issue attachments: %w", err)
82 | }
83 |
84 | // Convert to types.AttachmentList for consistent formatting
85 | attachmentList := make(types.AttachmentList, len(attachments))
86 | for i, attachment := range attachments {
87 | attachmentList[i] = &types.Attachment{Attachment: attachment}
88 | }
89 |
90 | return &mcp.CallToolResult{
91 | Content: []mcp.Content{
92 | &mcp.TextContent{
93 | Text: fmt.Sprintf("# Issue #%d Attachments\n\n%s", p.Index, attachmentList.ToMarkdown()),
94 | },
95 | },
96 | }, nil, nil
97 | }
98 | }
99 |
100 | // DeleteIssueAttachmentParams defines the parameters for deleting an issue attachment.
101 | // It specifies the attachment to be deleted by its ID.
102 | type DeleteIssueAttachmentParams struct {
103 | // Owner is the username or organization name that owns the repository.
104 | Owner string `json:"owner"`
105 | // Repo is the name of the repository.
106 | Repo string `json:"repo"`
107 | // Index is the issue number containing the attachment.
108 | Index int `json:"index"`
109 | // AttachmentID is the unique identifier of the attachment to delete.
110 | AttachmentID string `json:"attachment_id"`
111 | }
112 |
113 | // DeleteIssueAttachmentImpl implements the destructive MCP tool for deleting an issue attachment.
114 | // This is an idempotent but irreversible operation. Note: This feature is not supported
115 | // by the official Forgejo SDK and requires a custom HTTP implementation.
116 | type DeleteIssueAttachmentImpl struct {
117 | Client *tools.Client
118 | }
119 |
120 | // Definition describes the `delete_issue_attachment` tool. It requires the issue `index`
121 | // and `attachment_id`. It is marked as a destructive operation.
122 | func (DeleteIssueAttachmentImpl) Definition() *mcp.Tool {
123 | return &mcp.Tool{
124 | Name: "delete_issue_attachment",
125 | Title: "Delete Issue Attachment",
126 | Description: "Delete a specific attachment from an issue.",
127 | Annotations: &mcp.ToolAnnotations{
128 | ReadOnlyHint: false,
129 | DestructiveHint: tools.BoolPtr(true),
130 | IdempotentHint: true,
131 | },
132 | InputSchema: &jsonschema.Schema{
133 | Type: "object",
134 | Properties: map[string]*jsonschema.Schema{
135 | "owner": {
136 | Type: "string",
137 | Description: "Repository owner (username or organization name)",
138 | },
139 | "repo": {
140 | Type: "string",
141 | Description: "Repository name",
142 | },
143 | "index": {
144 | Type: "integer",
145 | Description: "Issue index number",
146 | },
147 | "attachment_id": {
148 | Type: "string",
149 | Description: "Attachment ID to delete",
150 | },
151 | },
152 | Required: []string{"owner", "repo", "index", "attachment_id"},
153 | },
154 | }
155 | }
156 |
157 | // Handler implements the logic for deleting an issue attachment. It performs a custom
158 | // HTTP DELETE request to the `/repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}`
159 | // endpoint. On success, it returns a simple text confirmation.
160 | func (impl DeleteIssueAttachmentImpl) Handler() mcp.ToolHandlerFor[DeleteIssueAttachmentParams, any] {
161 | return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteIssueAttachmentParams) (*mcp.CallToolResult, any, error) {
162 | p := args
163 |
164 | // Convert attachment ID from string to int64
165 | attachmentID, err := strconv.ParseInt(p.AttachmentID, 10, 64)
166 | if err != nil {
167 | return nil, nil, fmt.Errorf("invalid attachment ID: %w", err)
168 | }
169 |
170 | // Delete the attachment using the custom client method
171 | err = impl.Client.MyDeleteIssueAttachment(p.Owner, p.Repo, int64(p.Index), attachmentID)
172 | if err != nil {
173 | return nil, nil, fmt.Errorf("failed to delete issue attachment: %w", err)
174 | }
175 |
176 | return &mcp.CallToolResult{
177 | Content: []mcp.Content{
178 | &mcp.TextContent{
179 | Text: fmt.Sprintf("Issue attachment %s deleted successfully from issue #%d", p.AttachmentID, p.Index),
180 | },
181 | },
182 | }, nil, nil
183 | }
184 | }
185 |
186 | // EditIssueAttachmentParams defines the parameters for editing an issue attachment.
187 | // It specifies the attachment to edit and its new name.
188 | type EditIssueAttachmentParams struct {
189 | // Owner is the username or organization name that owns the repository.
190 | Owner string `json:"owner"`
191 | // Repo is the name of the repository.
192 | Repo string `json:"repo"`
193 | // Index is the issue number containing the attachment.
194 | Index int `json:"index"`
195 | // AttachmentID is the unique identifier of the attachment to edit.
196 | AttachmentID string `json:"attachment_id"`
197 | // Name is the new display name for the attachment.
198 | Name string `json:"name"`
199 | }
200 |
201 | // EditIssueAttachmentImpl implements the MCP tool for editing an issue attachment.
202 | // This is an idempotent operation. Note: This feature is not supported by the
203 | // official Forgejo SDK and requires a custom HTTP implementation.
204 | type EditIssueAttachmentImpl struct {
205 | Client *tools.Client
206 | }
207 |
208 | // Definition describes the `edit_issue_attachment` tool. It requires the issue `index`,
209 | // `attachment_id`, and a new `name`. It is marked as idempotent.
210 | func (EditIssueAttachmentImpl) Definition() *mcp.Tool {
211 | return &mcp.Tool{
212 | Name: "edit_issue_attachment",
213 | Title: "Edit Issue Attachment",
214 | Description: "Edit an attachment's metadata such as display name.",
215 | Annotations: &mcp.ToolAnnotations{
216 | ReadOnlyHint: false,
217 | DestructiveHint: tools.BoolPtr(false),
218 | IdempotentHint: true,
219 | },
220 | InputSchema: &jsonschema.Schema{
221 | Type: "object",
222 | Properties: map[string]*jsonschema.Schema{
223 | "owner": {
224 | Type: "string",
225 | Description: "Repository owner (username or organization name)",
226 | },
227 | "repo": {
228 | Type: "string",
229 | Description: "Repository name",
230 | },
231 | "index": {
232 | Type: "integer",
233 | Description: "Issue index number",
234 | },
235 | "attachment_id": {
236 | Type: "string",
237 | Description: "Attachment ID to edit",
238 | },
239 | "name": {
240 | Type: "string",
241 | Description: "New display name for the attachment",
242 | },
243 | },
244 | Required: []string{"owner", "repo", "index", "attachment_id", "name"},
245 | },
246 | }
247 | }
248 |
249 | // Handler implements the logic for editing an issue attachment. It performs a custom
250 | // HTTP PATCH request to the `/repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}`
251 | // endpoint. It will return an error if the attachment is not found.
252 | func (impl EditIssueAttachmentImpl) Handler() mcp.ToolHandlerFor[EditIssueAttachmentParams, any] {
253 | return func(ctx context.Context, req *mcp.CallToolRequest, args EditIssueAttachmentParams) (*mcp.CallToolResult, any, error) {
254 | p := args
255 |
256 | // Convert attachment ID from string to int64
257 | attachmentID, err := strconv.ParseInt(p.AttachmentID, 10, 64)
258 | if err != nil {
259 | return nil, nil, fmt.Errorf("invalid attachment ID: %w", err)
260 | }
261 |
262 | // Create options struct from parameters
263 | options := tools.MyEditAttachmentOptions{
264 | Name: p.Name,
265 | }
266 |
267 | // Edit the attachment using the custom client method
268 | attachment, err := impl.Client.MyEditIssueAttachment(p.Owner, p.Repo, int64(p.Index), attachmentID, options)
269 | if err != nil {
270 | return nil, nil, fmt.Errorf("failed to edit issue attachment: %w", err)
271 | }
272 |
273 | // Convert to types.Attachment for consistent formatting
274 | result := &types.Attachment{Attachment: attachment}
275 |
276 | return &mcp.CallToolResult{
277 | Content: []mcp.Content{
278 | &mcp.TextContent{
279 | Text: fmt.Sprintf("# Issue Attachment Updated\n\n%s", result.ToMarkdown()),
280 | },
281 | },
282 | }, nil, nil
283 | }
284 | }
285 |
```
--------------------------------------------------------------------------------
/tools/label/crud.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 label
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 | // ListRepoLabelsParams defines the parameters for the list_repo_labels tool.
22 | // It specifies the owner and repository name to list labels from.
23 | type ListRepoLabelsParams 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 | }
29 |
30 | // ListRepoLabelsImpl implements the read-only MCP tool for listing repository labels.
31 | // This operation is safe, idempotent, and does not modify any data. It fetches
32 | // all available labels for a specified repository using the Forgejo SDK.
33 | type ListRepoLabelsImpl struct {
34 | Client *tools.Client
35 | }
36 |
37 | // Definition describes the `list_repo_labels` tool. It requires `owner` and `repo`
38 | // as parameters and is marked as a safe, read-only operation.
39 | func (ListRepoLabelsImpl) Definition() *mcp.Tool {
40 | return &mcp.Tool{
41 | Name: "list_repo_labels",
42 | Title: "List Repository Labels",
43 | Description: "List all labels available in a repository. Returns label information including name, description, color, and ID.",
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 labels. It calls the Forgejo SDK's
66 | // `ListRepoLabels` function and formats the resulting slice of labels into
67 | // a markdown list. Errors will occur if the repository is not found or
68 | // authentication fails.
69 | func (impl ListRepoLabelsImpl) Handler() mcp.ToolHandlerFor[ListRepoLabelsParams, any] {
70 | return func(ctx context.Context, req *mcp.CallToolRequest, args ListRepoLabelsParams) (*mcp.CallToolResult, any, error) {
71 | p := args
72 |
73 | // Call SDK
74 | labels, _, err := impl.Client.ListRepoLabels(p.Owner, p.Repo, forgejo.ListLabelsOptions{})
75 | if err != nil {
76 | return nil, nil, fmt.Errorf("failed to list labels: %w", err)
77 | }
78 |
79 | // Convert to our types and format
80 | var content string
81 | if len(labels) == 0 {
82 | content = "No labels found in this repository."
83 | } else {
84 | // Convert labels to our type
85 | labelList := make(types.LabelList, len(labels))
86 | for i, label := range labels {
87 | labelList[i] = &types.Label{Label: label}
88 | }
89 |
90 | content = fmt.Sprintf("Found %d labels\n\n%s",
91 | len(labels), labelList.ToMarkdown())
92 | }
93 |
94 | return &mcp.CallToolResult{
95 | Content: []mcp.Content{
96 | &mcp.TextContent{
97 | Text: content,
98 | },
99 | },
100 | }, nil, nil
101 | }
102 | }
103 |
104 | // CreateLabelParams defines the parameters for the create_label tool.
105 | // It includes the label's name, color, and optional description.
106 | type CreateLabelParams struct {
107 | // Owner is the username or organization name that owns the repository.
108 | Owner string `json:"owner"`
109 | // Repo is the name of the repository.
110 | Repo string `json:"repo"`
111 | // Name is the name of the new label.
112 | Name string `json:"name"`
113 | // Color is the hex color code for the label (without the '#').
114 | Color string `json:"color"`
115 | // Description is the optional markdown description of the label.
116 | Description string `json:"description,omitempty"`
117 | }
118 |
119 | // CreateLabelImpl implements the MCP tool for creating a new repository label.
120 | // This is a non-idempotent operation that creates a new label using the Forgejo SDK.
121 | type CreateLabelImpl struct {
122 | Client *tools.Client
123 | }
124 |
125 | // Definition describes the `create_label` tool. It requires `owner`, `repo`,
126 | // a `name`, and a `color`. It is not idempotent, as multiple calls with the
127 | // same name will fail once the first label is created.
128 | func (CreateLabelImpl) Definition() *mcp.Tool {
129 | return &mcp.Tool{
130 | Name: "create_label",
131 | Title: "Create Label",
132 | Description: "Create a new label in a repository. Specify the label name, description, and color.",
133 | Annotations: &mcp.ToolAnnotations{
134 | ReadOnlyHint: false,
135 | DestructiveHint: tools.BoolPtr(false),
136 | IdempotentHint: false,
137 | },
138 | InputSchema: &jsonschema.Schema{
139 | Type: "object",
140 | Properties: map[string]*jsonschema.Schema{
141 | "owner": {
142 | Type: "string",
143 | Description: "Repository owner (username or organization name)",
144 | },
145 | "repo": {
146 | Type: "string",
147 | Description: "Repository name",
148 | },
149 | "name": {
150 | Type: "string",
151 | Description: "Label name",
152 | },
153 | "color": {
154 | Type: "string",
155 | Description: "Label color (hex color code without #, e.g., 'ff0000' for red)",
156 | },
157 | "description": {
158 | Type: "string",
159 | Description: "Optional label description",
160 | },
161 | },
162 | Required: []string{"owner", "repo", "name", "color"},
163 | },
164 | }
165 | }
166 |
167 | // Handler implements the logic for creating a label. It calls the Forgejo SDK's
168 | // `CreateLabel` function and returns the details of the newly created label.
169 | func (impl CreateLabelImpl) Handler() mcp.ToolHandlerFor[CreateLabelParams, any] {
170 | return func(ctx context.Context, req *mcp.CallToolRequest, args CreateLabelParams) (*mcp.CallToolResult, any, error) {
171 | p := args
172 |
173 | // Build options for SDK call
174 | opt := forgejo.CreateLabelOption{
175 | Name: p.Name,
176 | Color: p.Color,
177 | Description: p.Description,
178 | }
179 |
180 | // Call SDK
181 | label, _, err := impl.Client.CreateLabel(p.Owner, p.Repo, opt)
182 | if err != nil {
183 | return nil, nil, fmt.Errorf("failed to create label: %w", err)
184 | }
185 |
186 | // Convert to our type and format
187 | labelWrapper := &types.Label{Label: label}
188 |
189 | return &mcp.CallToolResult{
190 | Content: []mcp.Content{
191 | &mcp.TextContent{
192 | Text: labelWrapper.ToMarkdown(),
193 | },
194 | },
195 | }, nil, nil
196 | }
197 | }
198 |
199 | // EditLabelParams defines the parameters for the edit_label tool.
200 | // It specifies the label to edit by ID and the fields to update.
201 | type EditLabelParams struct {
202 | // Owner is the username or organization name that owns the repository.
203 | Owner string `json:"owner"`
204 | // Repo is the name of the repository.
205 | Repo string `json:"repo"`
206 | // ID is the unique identifier of the label to edit.
207 | ID int `json:"id"`
208 | // Name is the new name for the label.
209 | Name string `json:"name,omitempty"`
210 | // Color is the new hex color code for the label (without the '#').
211 | Color string `json:"color,omitempty"`
212 | // Description is the new optional markdown description for the label.
213 | Description string `json:"description,omitempty"`
214 | }
215 |
216 | // EditLabelImpl implements the MCP tool for editing an existing repository label.
217 | // This is an idempotent operation that modifies a label's metadata using the
218 | // Forgejo SDK.
219 | type EditLabelImpl struct {
220 | Client *tools.Client
221 | }
222 |
223 | // Definition describes the `edit_label` tool. It requires `owner`, `repo`, and the
224 | // label `id`. It is marked as idempotent.
225 | func (EditLabelImpl) Definition() *mcp.Tool {
226 | return &mcp.Tool{
227 | Name: "edit_label",
228 | Title: "Edit Label",
229 | Description: "Edit an existing label's name, description, or color.",
230 | Annotations: &mcp.ToolAnnotations{
231 | ReadOnlyHint: false,
232 | DestructiveHint: tools.BoolPtr(false),
233 | IdempotentHint: true,
234 | },
235 | InputSchema: &jsonschema.Schema{
236 | Type: "object",
237 | Properties: map[string]*jsonschema.Schema{
238 | "owner": {
239 | Type: "string",
240 | Description: "Repository owner (username or organization name)",
241 | },
242 | "repo": {
243 | Type: "string",
244 | Description: "Repository name",
245 | },
246 | "id": {
247 | Type: "integer",
248 | Description: "Label ID",
249 | },
250 | "name": {
251 | Type: "string",
252 | Description: "New label name (optional)",
253 | },
254 | "color": {
255 | Type: "string",
256 | Description: "New label color (hex color code without #, e.g., 'ff0000' for red) (optional)",
257 | },
258 | "description": {
259 | Type: "string",
260 | Description: "New label description (optional)",
261 | },
262 | },
263 | Required: []string{"owner", "repo", "id"},
264 | },
265 | }
266 | }
267 |
268 | // Handler implements the logic for editing a label. It calls the Forgejo SDK's
269 | // `EditLabel` function. It will return an error if the label ID is not found.
270 | func (impl EditLabelImpl) Handler() mcp.ToolHandlerFor[EditLabelParams, any] {
271 | return func(ctx context.Context, req *mcp.CallToolRequest, args EditLabelParams) (*mcp.CallToolResult, any, error) {
272 | p := args
273 |
274 | // Build options for SDK call
275 | opt := forgejo.EditLabelOption{}
276 | if p.Name != "" {
277 | opt.Name = &p.Name
278 | }
279 | if p.Color != "" {
280 | opt.Color = &p.Color
281 | }
282 | if p.Description != "" {
283 | opt.Description = &p.Description
284 | }
285 |
286 | // Call SDK
287 | label, _, err := impl.Client.EditLabel(p.Owner, p.Repo, int64(p.ID), opt)
288 | if err != nil {
289 | return nil, nil, fmt.Errorf("failed to edit label: %w", err)
290 | }
291 |
292 | // Convert to our type and format
293 | labelWrapper := &types.Label{Label: label}
294 |
295 | return &mcp.CallToolResult{
296 | Content: []mcp.Content{
297 | &mcp.TextContent{
298 | Text: labelWrapper.ToMarkdown(),
299 | },
300 | },
301 | }, nil, nil
302 | }
303 | }
304 |
305 | // DeleteLabelParams defines the parameters for the delete_label tool.
306 | // It specifies the label to be deleted by its ID.
307 | type DeleteLabelParams struct {
308 | // Owner is the username or organization name that owns the repository.
309 | Owner string `json:"owner"`
310 | // Repo is the name of the repository.
311 | Repo string `json:"repo"`
312 | // ID is the unique identifier of the label to delete.
313 | ID int `json:"id"`
314 | }
315 |
316 | // DeleteLabelImpl implements the destructive MCP tool for deleting a repository label.
317 | // This is an idempotent but irreversible operation that removes a label from a
318 | // repository using the Forgejo SDK.
319 | type DeleteLabelImpl struct {
320 | Client *tools.Client
321 | }
322 |
323 | // Definition describes the `delete_label` tool. It requires `owner`, `repo`, and
324 | // the label `id`. It is marked as a destructive operation to ensure clients
325 | // can warn the user before execution.
326 | func (DeleteLabelImpl) Definition() *mcp.Tool {
327 | return &mcp.Tool{
328 | Name: "delete_label",
329 | Title: "Delete Label",
330 | Description: "Delete a label from a repository.",
331 | Annotations: &mcp.ToolAnnotations{
332 | ReadOnlyHint: false,
333 | DestructiveHint: tools.BoolPtr(true),
334 | IdempotentHint: true,
335 | },
336 | InputSchema: &jsonschema.Schema{
337 | Type: "object",
338 | Properties: map[string]*jsonschema.Schema{
339 | "owner": {
340 | Type: "string",
341 | Description: "Repository owner (username or organization name)",
342 | },
343 | "repo": {
344 | Type: "string",
345 | Description: "Repository name",
346 | },
347 | "id": {
348 | Type: "integer",
349 | Description: "Label ID to delete",
350 | },
351 | },
352 | Required: []string{"owner", "repo", "id"},
353 | },
354 | }
355 | }
356 |
357 | // Handler implements the logic for deleting a label. It calls the Forgejo SDK's
358 | // `DeleteLabel` function. On success, it returns a simple text confirmation.
359 | // It will return an error if the label does not exist.
360 | func (impl DeleteLabelImpl) Handler() mcp.ToolHandlerFor[DeleteLabelParams, any] {
361 | return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteLabelParams) (*mcp.CallToolResult, any, error) {
362 | p := args
363 |
364 | // Call SDK
365 | _, err := impl.Client.DeleteLabel(p.Owner, p.Repo, int64(p.ID))
366 | if err != nil {
367 | return nil, nil, fmt.Errorf("failed to delete label: %w", err)
368 | }
369 |
370 | // Return success message
371 | emptyResponse := types.EmptyResponse{}
372 |
373 | return &mcp.CallToolResult{
374 | Content: []mcp.Content{
375 | &mcp.TextContent{
376 | Text: emptyResponse.ToMarkdown(),
377 | },
378 | },
379 | }, nil, nil
380 | }
381 | }
382 |
```
--------------------------------------------------------------------------------
/tools/wiki/crud.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 | "encoding/base64"
12 | "fmt"
13 |
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 | // GetWikiPageParams defines the parameters for the get_wiki_page tool.
22 | // It specifies the owner, repository, and page name to retrieve.
23 | type GetWikiPageParams 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 | // PageName is the title of the wiki page to retrieve.
29 | PageName string `json:"page_name"`
30 | }
31 |
32 | // GetWikiPageImpl implements the read-only MCP tool for fetching a single wiki page.
33 | // This operation is safe, idempotent, and does not modify any data. Note: This
34 | // feature is not supported by the official Forgejo SDK and requires a custom
35 | // HTTP implementation.
36 | type GetWikiPageImpl struct {
37 | Client *tools.Client
38 | }
39 |
40 | // Definition describes the `get_wiki_page` tool. It requires `owner`, `repo`,
41 | // and `page_name` as parameters and is marked as a safe, read-only operation.
42 | func (GetWikiPageImpl) Definition() *mcp.Tool {
43 | return &mcp.Tool{
44 | Name: "get_wiki_page",
45 | Title: "Get Wiki Page",
46 | Description: "Get the content and metadata of a specific wiki page.",
47 | Annotations: &mcp.ToolAnnotations{
48 | ReadOnlyHint: true,
49 | IdempotentHint: true,
50 | },
51 | InputSchema: &jsonschema.Schema{
52 | Type: "object",
53 | Properties: map[string]*jsonschema.Schema{
54 | "owner": {
55 | Type: "string",
56 | Description: "Repository owner (username or organization name)",
57 | },
58 | "repo": {
59 | Type: "string",
60 | Description: "Repository name",
61 | },
62 | "page_name": {
63 | Type: "string",
64 | Description: "Wiki page name",
65 | },
66 | },
67 | Required: []string{"owner", "repo", "page_name"},
68 | },
69 | }
70 | }
71 |
72 | // Handler implements the logic for fetching a wiki page. It performs a custom
73 | // HTTP GET request to the `/repos/{owner}/{repo}/wiki/page/{pageName}` endpoint
74 | // and formats the resulting page content as markdown. Errors will occur if the
75 | // page or repository is not found.
76 | func (impl GetWikiPageImpl) Handler() mcp.ToolHandlerFor[GetWikiPageParams, any] {
77 | return func(ctx context.Context, req *mcp.CallToolRequest, args GetWikiPageParams) (*mcp.CallToolResult, any, error) {
78 | p := args
79 |
80 | // Call custom client method
81 | page, err := impl.Client.MyGetWikiPage(p.Owner, p.Repo, p.PageName)
82 | if err != nil {
83 | return nil, nil, fmt.Errorf("failed to get wiki page: %w", err)
84 | }
85 |
86 | // Convert to our type and format
87 | wikiPage := &types.WikiPage{
88 | MyWikiPage: page,
89 | }
90 |
91 | return &mcp.CallToolResult{
92 | Content: []mcp.Content{
93 | &mcp.TextContent{
94 | Text: wikiPage.ToMarkdown(),
95 | },
96 | },
97 | }, nil, nil
98 | }
99 | }
100 |
101 | // CreateWikiPageParams defines the parameters for the create_wiki_page tool.
102 | // It includes the title and content for the new wiki page.
103 | type CreateWikiPageParams struct {
104 | // Owner is the username or organization name that owns the repository.
105 | Owner string `json:"owner"`
106 | // Repo is the name of the repository.
107 | Repo string `json:"repo"`
108 | // Title is the title for the new wiki page.
109 | Title string `json:"title"`
110 | // Content is the markdown content of the new wiki page.
111 | Content string `json:"content"`
112 | // Message is an optional commit message for the creation.
113 | Message string `json:"message,omitempty"`
114 | }
115 |
116 | // CreateWikiPageImpl implements the MCP tool for creating a new wiki page.
117 | // This is a non-idempotent operation that adds a new page to the repository's
118 | // wiki. Note: This feature is not supported by the official Forgejo SDK and
119 | // requires a custom HTTP implementation.
120 | type CreateWikiPageImpl struct {
121 | Client *tools.Client
122 | }
123 |
124 | // Definition describes the `create_wiki_page` tool. It requires `owner`, `repo`,
125 | // `title`, and `content`. It is not idempotent as multiple calls with the same
126 | // parameters will result in multiple pages if the title is not unique.
127 | func (CreateWikiPageImpl) Definition() *mcp.Tool {
128 | return &mcp.Tool{
129 | Name: "create_wiki_page",
130 | Title: "Create Wiki Page",
131 | Description: "Create a new wiki page with specified title and content.",
132 | Annotations: &mcp.ToolAnnotations{
133 | ReadOnlyHint: false,
134 | DestructiveHint: tools.BoolPtr(false),
135 | IdempotentHint: false,
136 | },
137 | InputSchema: &jsonschema.Schema{
138 | Type: "object",
139 | Properties: map[string]*jsonschema.Schema{
140 | "owner": {
141 | Type: "string",
142 | Description: "Repository owner (username or organization name)",
143 | },
144 | "repo": {
145 | Type: "string",
146 | Description: "Repository name",
147 | },
148 | "title": {
149 | Type: "string",
150 | Description: "Wiki page title",
151 | },
152 | "content": {
153 | Type: "string",
154 | Description: "Wiki page content (markdown supported)",
155 | },
156 | "message": {
157 | Type: "string",
158 | Description: "Optional commit message (defaults to 'Create page {title}')",
159 | },
160 | },
161 | Required: []string{"owner", "repo", "title", "content"},
162 | },
163 | }
164 | }
165 |
166 | // Handler implements the logic for creating a wiki page. It performs a custom
167 | // HTTP POST request to the `/repos/{owner}/{repo}/wiki/new` endpoint. On success,
168 | // it returns information about the newly created page.
169 | func (impl CreateWikiPageImpl) Handler() mcp.ToolHandlerFor[CreateWikiPageParams, any] {
170 | return func(ctx context.Context, req *mcp.CallToolRequest, args CreateWikiPageParams) (*mcp.CallToolResult, any, error) {
171 | p := args
172 |
173 | // Prepare options for API call
174 | options := types.MyCreateWikiPageOptions{
175 | Title: p.Title,
176 | ContentBase64: base64.StdEncoding.EncodeToString([]byte(p.Content)),
177 | Message: p.Message,
178 | }
179 |
180 | // Call custom client method
181 | page, err := impl.Client.MyCreateWikiPage(p.Owner, p.Repo, options)
182 | if err != nil {
183 | return nil, nil, fmt.Errorf("failed to create wiki page: %w", err)
184 | }
185 |
186 | // Convert to our type and format
187 | wikiPage := &types.WikiPage{
188 | MyWikiPage: page,
189 | }
190 |
191 | return &mcp.CallToolResult{
192 | Content: []mcp.Content{
193 | &mcp.TextContent{
194 | Text: wikiPage.ToMarkdown(),
195 | },
196 | },
197 | }, nil, nil
198 | }
199 | }
200 |
201 | // EditWikiPageParams defines the parameters for the edit_wiki_page tool.
202 | // It specifies the page to edit and the new content.
203 | type EditWikiPageParams struct {
204 | // Owner is the username or organization name that owns the repository.
205 | Owner string `json:"owner"`
206 | // Repo is the name of the repository.
207 | Repo string `json:"repo"`
208 | // PageName is the current title of the wiki page to edit.
209 | PageName string `json:"page_name"`
210 | // Title is the optional new title for the wiki page.
211 | Title string `json:"title,omitempty"`
212 | // Content is the new markdown content for the wiki page.
213 | Content string `json:"content"`
214 | // Message is an optional commit message for the update.
215 | Message string `json:"message,omitempty"`
216 | }
217 |
218 | // EditWikiPageImpl implements the MCP tool for editing an existing wiki page.
219 | // This is an idempotent operation. Note: This feature is not supported by the
220 | // official Forgejo SDK and requires a custom HTTP implementation.
221 | type EditWikiPageImpl struct {
222 | Client *tools.Client
223 | }
224 |
225 | // Definition describes the `edit_wiki_page` tool. It requires `owner`, `repo`,
226 | // `page_name`, and new `content`. It is marked as idempotent.
227 | func (EditWikiPageImpl) Definition() *mcp.Tool {
228 | return &mcp.Tool{
229 | Name: "edit_wiki_page",
230 | Title: "Edit Wiki Page",
231 | Description: "Edit an existing wiki page's title and content.",
232 | Annotations: &mcp.ToolAnnotations{
233 | ReadOnlyHint: false,
234 | DestructiveHint: tools.BoolPtr(false),
235 | IdempotentHint: true,
236 | },
237 | InputSchema: &jsonschema.Schema{
238 | Type: "object",
239 | Properties: map[string]*jsonschema.Schema{
240 | "owner": {
241 | Type: "string",
242 | Description: "Repository owner (username or organization name)",
243 | },
244 | "repo": {
245 | Type: "string",
246 | Description: "Repository name",
247 | },
248 | "page_name": {
249 | Type: "string",
250 | Description: "Wiki page name to edit",
251 | },
252 | "title": {
253 | Type: "string",
254 | Description: "New wiki page title (optional, defaults to current title)",
255 | },
256 | "content": {
257 | Type: "string",
258 | Description: "New wiki page content (markdown supported)",
259 | },
260 | "message": {
261 | Type: "string",
262 | Description: "Optional commit message (defaults to 'Update page {page_name}')",
263 | },
264 | },
265 | Required: []string{"owner", "repo", "page_name", "content"},
266 | },
267 | }
268 | }
269 |
270 | // Handler implements the logic for editing a wiki page. It performs a custom
271 | // HTTP PATCH request to the `/repos/{owner}/{repo}/wiki/page/{pageName}` endpoint.
272 | // It returns an error if the page is not found.
273 | func (impl EditWikiPageImpl) Handler() mcp.ToolHandlerFor[EditWikiPageParams, any] {
274 | return func(ctx context.Context, req *mcp.CallToolRequest, args EditWikiPageParams) (*mcp.CallToolResult, any, error) {
275 | p := args
276 |
277 | // Prepare options for API call
278 | title := p.Title
279 | if title == "" {
280 | title = p.PageName // Use current name if no new title
281 | }
282 | options := types.MyCreateWikiPageOptions{
283 | Title: title,
284 | ContentBase64: base64.StdEncoding.EncodeToString([]byte(p.Content)),
285 | Message: p.Message,
286 | }
287 |
288 | // Call custom client method
289 | page, err := impl.Client.MyEditWikiPage(p.Owner, p.Repo, p.PageName, options)
290 | if err != nil {
291 | return nil, nil, fmt.Errorf("failed to edit wiki page: %w", err)
292 | }
293 |
294 | // Convert to our type and format
295 | wikiPage := &types.WikiPage{
296 | MyWikiPage: page,
297 | }
298 |
299 | return &mcp.CallToolResult{
300 | Content: []mcp.Content{
301 | &mcp.TextContent{
302 | Text: wikiPage.ToMarkdown(),
303 | },
304 | },
305 | }, nil, nil
306 | }
307 | }
308 |
309 | // DeleteWikiPageParams defines the parameters for the delete_wiki_page tool.
310 | // It specifies the page to be deleted.
311 | type DeleteWikiPageParams struct {
312 | // Owner is the username or organization name that owns the repository.
313 | Owner string `json:"owner"`
314 | // Repo is the name of the repository.
315 | Repo string `json:"repo"`
316 | // PageName is the title of the wiki page to delete.
317 | PageName string `json:"page_name"`
318 | }
319 |
320 | // DeleteWikiPageImpl implements the destructive MCP tool for deleting a wiki page.
321 | // This is an idempotent but irreversible operation. Note: This feature is not
322 | // supported by the official Forgejo SDK and requires a custom HTTP implementation.
323 | type DeleteWikiPageImpl struct {
324 | Client *tools.Client
325 | }
326 |
327 | // Definition describes the `delete_wiki_page` tool. It requires `owner`, `repo`,
328 | // and `page_name`. It is marked as a destructive operation to ensure clients
329 | // can warn the user before execution.
330 | func (DeleteWikiPageImpl) Definition() *mcp.Tool {
331 | return &mcp.Tool{
332 | Name: "delete_wiki_page",
333 | Title: "Delete Wiki Page",
334 | Description: "Delete a wiki page from the repository.",
335 | Annotations: &mcp.ToolAnnotations{
336 | ReadOnlyHint: false,
337 | DestructiveHint: tools.BoolPtr(true),
338 | IdempotentHint: true,
339 | },
340 | InputSchema: &jsonschema.Schema{
341 | Type: "object",
342 | Properties: map[string]*jsonschema.Schema{
343 | "owner": {
344 | Type: "string",
345 | Description: "Repository owner (username or organization name)",
346 | },
347 | "repo": {
348 | Type: "string",
349 | Description: "Repository name",
350 | },
351 | "page_name": {
352 | Type: "string",
353 | Description: "Wiki page name to delete",
354 | },
355 | },
356 | Required: []string{"owner", "repo", "page_name"},
357 | },
358 | }
359 | }
360 |
361 | // Handler implements the logic for deleting a wiki page. It performs a custom
362 | // HTTP DELETE request to the `/repos/{owner}/{repo}/wiki/page/{pageName}` endpoint.
363 | // On success, it returns a simple text confirmation.
364 | func (impl DeleteWikiPageImpl) Handler() mcp.ToolHandlerFor[DeleteWikiPageParams, any] {
365 | return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteWikiPageParams) (*mcp.CallToolResult, any, error) {
366 | p := args
367 |
368 | // Call custom client method
369 | err := impl.Client.MyDeleteWikiPage(p.Owner, p.Repo, p.PageName)
370 | if err != nil {
371 | return nil, nil, fmt.Errorf("failed to delete wiki page: %w", err)
372 | }
373 |
374 | // Return success message
375 | emptyResponse := types.EmptyResponse{}
376 |
377 | return &mcp.CallToolResult{
378 | Content: []mcp.Content{
379 | &mcp.TextContent{
380 | Text: emptyResponse.ToMarkdown(),
381 | },
382 | },
383 | }, nil, nil
384 | }
385 | }
386 |
```
--------------------------------------------------------------------------------
/tools/client_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 tools
8 |
9 | import (
10 | "encoding/json"
11 | "net/http"
12 | "net/http/httptest"
13 | "os"
14 | "strings"
15 | "testing"
16 | )
17 |
18 | const forgejo_version_to_test = "11.0.1+gitea-1.22.0"
19 |
20 | // sendSimpleRequest Specification:
21 | //
22 | // Responsibility: Handle all pure JSON API requests, including Issue Dependencies,
23 | // Wiki Pages, Forgejo Actions, and Issue Attachments non-upload operations
24 | //
25 | // Business Logic:
26 | // 1. Create HTTP request (using provided method and endpoint)
27 | // 2. If paramObj is not nil, serialize it as JSON for request body
28 | // 3. Use Forgejo SDK's SignRequest method to add authentication headers
29 | // 4. Send request and receive response
30 | // 5. Deserialize JSON response to respObj
31 | // 6. Handle HTTP error status codes
32 | //
33 | // Parameters:
34 | // - method: HTTP method (GET, POST, PATCH, DELETE)
35 | // - endpoint: API endpoint path (relative to base URL)
36 | // - paramObj: request parameter object (JSON serialized), can be nil for GET/DELETE
37 | // - respObj: response data receiver object (JSON deserialized)
38 | //
39 | // Returns: error if request fails or response cannot be parsed
40 | //
41 | // Design Philosophy:
42 | // - Focus on our project needs, not pursuing genericity
43 | // - Rely on Forgejo SDK for authentication, we only handle request/response serialization
44 | // - Simplified error handling, mainly focus on network errors and JSON parsing errors
45 | //
46 | // Implementation Notes:
47 | // - Need to combine c.base and endpoint to form complete URL
48 | // - Content-Type should be set to application/json (when request body exists)
49 | // - Accept should be set to application/json
50 | // - HTTP 4xx/5xx status codes should return errors
51 | func TestClient_sendSimpleRequest(t *testing.T) {
52 | // GET request test - no request body
53 | t.Run("GET_success", func(t *testing.T) {
54 | // Mock server
55 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56 | if r.Method != "GET" {
57 | t.Errorf("Expected GET method, got %s", r.Method)
58 | }
59 | if r.URL.Path != "/api/v1/repos/owner/repo/issues/1/dependencies" {
60 | t.Errorf("Expected specific path, got %s", r.URL.Path)
61 | }
62 | w.Header().Set("Content-Type", "application/json")
63 | json.NewEncoder(w).Encode(map[string]interface{}{
64 | "id": 1,
65 | "title": "Test Issue",
66 | })
67 | }))
68 | defer server.Close()
69 |
70 | client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
71 | if err != nil {
72 | t.Fatalf("Failed to create client: %v", err)
73 | }
74 |
75 | var result map[string]interface{}
76 | err = client.sendSimpleRequest("GET", "/api/v1/repos/owner/repo/issues/1/dependencies", nil, &result)
77 |
78 | if err != nil {
79 | t.Errorf("Expected no error, got %v", err)
80 | }
81 | if result["id"] != float64(1) {
82 | t.Errorf("Expected id=1, got %v", result["id"])
83 | }
84 | if result["title"] != "Test Issue" {
85 | t.Errorf("Expected title='Test Issue', got %v", result["title"])
86 | }
87 | })
88 |
89 | // POST request test - with request body
90 | t.Run("POST_success", func(t *testing.T) {
91 | // Mock server
92 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
93 | if r.Method != "POST" {
94 | t.Errorf("Expected POST method, got %s", r.Method)
95 | }
96 | if r.Header.Get("Content-Type") != "application/json" {
97 | t.Errorf("Expected Content-Type: application/json, got %s", r.Header.Get("Content-Type"))
98 | }
99 |
100 | var reqBody map[string]interface{}
101 | json.NewDecoder(r.Body).Decode(&reqBody)
102 | if reqBody["index"] != float64(2) {
103 | t.Errorf("Expected index=2, got %v", reqBody["index"])
104 | }
105 |
106 | w.Header().Set("Content-Type", "application/json")
107 | json.NewEncoder(w).Encode(map[string]interface{}{
108 | "message": "dependency added",
109 | })
110 | }))
111 | defer server.Close()
112 |
113 | client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
114 | if err != nil {
115 | t.Fatalf("Failed to create client: %v", err)
116 | }
117 |
118 | requestData := map[string]interface{}{"index": 2}
119 | var result map[string]interface{}
120 | err = client.sendSimpleRequest("POST", "/api/v1/repos/owner/repo/issues/1/dependencies", requestData, &result)
121 |
122 | if err != nil {
123 | t.Errorf("Expected no error, got %v", err)
124 | }
125 | if result["message"] != "dependency added" {
126 | t.Errorf("Expected message='dependency added', got %v", result["message"])
127 | }
128 | })
129 |
130 | // HTTP error handling test
131 | t.Run("HTTP_error", func(t *testing.T) {
132 | // Mock server returning 404
133 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
134 | w.WriteHeader(http.StatusNotFound)
135 | json.NewEncoder(w).Encode(map[string]interface{}{
136 | "error": "Not found",
137 | })
138 | }))
139 | defer server.Close()
140 |
141 | client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
142 | if err != nil {
143 | t.Fatalf("Failed to create client: %v", err)
144 | }
145 |
146 | var result map[string]interface{}
147 | err = client.sendSimpleRequest("GET", "/api/v1/repos/owner/repo/nonexistent", nil, &result)
148 |
149 | if err == nil {
150 | t.Error("Expected error for 404 response, got nil")
151 | }
152 | })
153 |
154 | // JSON parsing error test
155 | t.Run("JSON_parse_error", func(t *testing.T) {
156 | // Mock server returning invalid JSON
157 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
158 | w.Header().Set("Content-Type", "application/json")
159 | w.Write([]byte("invalid json"))
160 | }))
161 | defer server.Close()
162 |
163 | client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
164 | if err != nil {
165 | t.Fatalf("Failed to create client: %v", err)
166 | }
167 |
168 | var result map[string]interface{}
169 | err = client.sendSimpleRequest("GET", "/api/v1/repos/owner/repo/issues", nil, &result)
170 |
171 | if err == nil {
172 | t.Error("Expected JSON parsing error, got nil")
173 | }
174 | })
175 | }
176 |
177 | // sendUploadRequest Specification:
178 | //
179 | // Responsibility: Handle file upload requests, currently mainly for Issue Attachment creation
180 | //
181 | // Business Logic:
182 | // 1. Create multipart/form-data format HTTP POST request
183 | // 2. Add file to multipart writer (using filename)
184 | // 3. Add additional form fields from extraFields
185 | // 4. Use Forgejo SDK's SignRequest method to add authentication headers
186 | // 5. Send request and receive response
187 | // 6. Deserialize JSON response to respObj
188 | //
189 | // Parameters:
190 | // - endpoint: API endpoint path (fixed to use POST method)
191 | // - filename: upload file name
192 | // - file: file content (io.Reader)
193 | // - extraFields: additional form fields (e.g. name, updated_at)
194 | // - respObj: response data receiver object
195 | //
196 | // Returns: error if upload fails or response cannot be parsed
197 | //
198 | // Design Philosophy:
199 | // - Focus on Issue Attachment upload requirements
200 | // - Use standard multipart/form-data format
201 | // - Rely on Forgejo SDK for authentication
202 | //
203 | // Implementation Notes:
204 | // - Content-Type will be automatically set by multipart.Writer
205 | // - File field name should be "attachment" (according to Forgejo API)
206 | // - Accept should be set to application/json
207 | // - Need to correctly handle multipart boundary
208 | func TestClient_sendUploadRequest(t *testing.T) {
209 | // Successful upload test
210 | t.Run("upload_success", func(t *testing.T) {
211 | // Mock server
212 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
213 | if r.Method != "POST" {
214 | t.Errorf("Expected POST method, got %s", r.Method)
215 | }
216 | if !strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") {
217 | t.Errorf("Expected multipart/form-data Content-Type, got %s", r.Header.Get("Content-Type"))
218 | }
219 |
220 | // Parse multipart form
221 | err := r.ParseMultipartForm(32 << 20) // 32MB
222 | if err != nil {
223 | t.Errorf("Failed to parse multipart form: %v", err)
224 | }
225 |
226 | // Check extra fields
227 | if r.FormValue("name") != "test.txt" {
228 | t.Errorf("Expected name='test.txt', got %s", r.FormValue("name"))
229 | }
230 |
231 | // Check file
232 | file, header, err := r.FormFile("attachment")
233 | if err != nil {
234 | t.Errorf("Failed to get file: %v", err)
235 | }
236 | defer file.Close()
237 |
238 | if header.Filename != "test.txt" {
239 | t.Errorf("Expected filename='test.txt', got %s", header.Filename)
240 | }
241 |
242 | w.Header().Set("Content-Type", "application/json")
243 | json.NewEncoder(w).Encode(map[string]interface{}{
244 | "id": "123",
245 | "name": "test.txt",
246 | "download_url": "http://example.com/download/123",
247 | })
248 | }))
249 | defer server.Close()
250 |
251 | client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
252 | if err != nil {
253 | t.Fatalf("Failed to create client: %v", err)
254 | }
255 |
256 | file := strings.NewReader("test file content")
257 | extraFields := map[string]string{"name": "test.txt"}
258 | var result map[string]interface{}
259 |
260 | err = client.sendUploadRequest("/api/v1/repos/owner/repo/issues/1/assets", "test.txt", file, extraFields, &result)
261 |
262 | if err != nil {
263 | t.Errorf("Expected no error, got %v", err)
264 | }
265 | if result["id"] != "123" {
266 | t.Errorf("Expected id='123', got %v", result["id"])
267 | }
268 | if result["name"] != "test.txt" {
269 | t.Errorf("Expected name='test.txt', got %v", result["name"])
270 | }
271 | })
272 |
273 | // Empty file upload test
274 | t.Run("empty_file", func(t *testing.T) {
275 | // Mock server
276 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
277 | if r.Method != "POST" {
278 | t.Errorf("Expected POST method, got %s", r.Method)
279 | }
280 |
281 | err := r.ParseMultipartForm(32 << 20)
282 | if err != nil {
283 | t.Errorf("Failed to parse multipart form: %v", err)
284 | }
285 |
286 | file, header, err := r.FormFile("attachment")
287 | if err != nil {
288 | t.Errorf("Failed to get file: %v", err)
289 | }
290 | defer file.Close()
291 |
292 | if header.Filename != "empty.txt" {
293 | t.Errorf("Expected filename='empty.txt', got %s", header.Filename)
294 | }
295 |
296 | w.Header().Set("Content-Type", "application/json")
297 | json.NewEncoder(w).Encode(map[string]interface{}{
298 | "id": "124",
299 | "name": "empty.txt",
300 | })
301 | }))
302 | defer server.Close()
303 |
304 | client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
305 | if err != nil {
306 | t.Fatalf("Failed to create client: %v", err)
307 | }
308 |
309 | file := strings.NewReader("")
310 | var result map[string]interface{}
311 |
312 | err = client.sendUploadRequest("/api/v1/repos/owner/repo/issues/1/assets", "empty.txt", file, nil, &result)
313 |
314 | if err != nil {
315 | t.Errorf("Expected no error for empty file, got %v", err)
316 | }
317 | if result["id"] != "124" {
318 | t.Errorf("Expected id='124', got %v", result["id"])
319 | }
320 | })
321 |
322 | // Upload error test
323 | t.Run("upload_error", func(t *testing.T) {
324 | // Mock server returning 500 error
325 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
326 | w.WriteHeader(http.StatusInternalServerError)
327 | json.NewEncoder(w).Encode(map[string]interface{}{
328 | "error": "Internal server error",
329 | })
330 | }))
331 | defer server.Close()
332 |
333 | client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
334 | if err != nil {
335 | t.Fatalf("Failed to create client: %v", err)
336 | }
337 |
338 | file := strings.NewReader("test content")
339 | var result map[string]interface{}
340 |
341 | err = client.sendUploadRequest("/api/v1/repos/owner/repo/issues/1/assets", "test.txt", file, nil, &result)
342 |
343 | if err == nil {
344 | t.Error("Expected error for 500 response, got nil")
345 | }
346 | })
347 | }
348 |
349 | // DO NOT TEST AGAINST PRODUCTION FORGEJO SERVERS
350 | // DO NOT TEST AGAINST REPO THAT HAS MORE THAN 50 WORKFLOW RUNS
351 | func TestCustomClient_Integral(t *testing.T) {
352 | server := os.Getenv("FORGEJO_TEST_SERVER")
353 | token := os.Getenv("FORGEJO_TEST_TOKEN")
354 | repo := os.Getenv("FORGEJO_TEST_REPO")
355 | if server == "" || token == "" || repo == "" {
356 | t.Skip("Skipping test, FORGEJO_TEST_SERVER, FORGEJO_TEST_TOKEN, and FORGEJO_TEST_REPO must be set")
357 | }
358 | arr := strings.Split(repo, "/")
359 | if len(arr) != 2 {
360 | t.Fatalf("Invalid repo format, expected 'owner/repo', got '%s'", repo)
361 | }
362 |
363 | t.Logf("Using server: %s, repo: %s", server, repo)
364 | cl, err := NewClient(server, token, "", nil)
365 | if err != nil {
366 | t.Fatalf("Failed to create client: %v", err)
367 | }
368 |
369 | resp, err := cl.MyListActionTasks(arr[0], arr[1])
370 | if err != nil {
371 | t.Fatalf("Failed to list action tasks: %v", err)
372 | }
373 |
374 | if resp == nil {
375 | t.Fatal("Expected non-nil response, got nil")
376 | }
377 |
378 | if resp.TotalCount < 0 {
379 | t.Errorf("Expected TotalCount >= 0, got %d", resp.TotalCount)
380 | }
381 |
382 | if len(resp.WorkflowRuns) != int(resp.TotalCount) {
383 | t.Errorf("Expected WorkflowRuns length %d, got %d", resp.TotalCount, len(resp.WorkflowRuns))
384 | }
385 | if len(resp.WorkflowRuns) > 0 {
386 | t.Logf("First WorkflowRun ID: %d, Name: %s, Status: %s", resp.WorkflowRuns[0].ID, resp.WorkflowRuns[0].Name, resp.WorkflowRuns[0].Status)
387 | }
388 | }
389 |
```
--------------------------------------------------------------------------------
/tools/issue/comment.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 issue
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 | // ListIssueCommentsParams defines the parameters for the list_issue_comments tool.
23 | // It specifies the issue and includes optional filters for pagination and time range.
24 | type ListIssueCommentsParams 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 | // Index is the issue number.
30 | Index int `json:"index"`
31 | // Since filters for comments updated after the given time.
32 | Since time.Time `json:"since,omitempty"`
33 | // Before filters for comments updated before the given time.
34 | Before time.Time `json:"before,omitempty"`
35 | // Page is the page number for pagination.
36 | Page int `json:"page,omitempty"`
37 | // Limit is the number of comments to return per page.
38 | Limit int `json:"limit,omitempty"`
39 | }
40 |
41 | // ListIssueCommentsImpl implements the read-only MCP tool for listing issue comments.
42 | // This is a safe, idempotent operation that uses the Forgejo SDK to fetch a list
43 | // of comments for a specific issue.
44 | type ListIssueCommentsImpl struct {
45 | Client *tools.Client
46 | }
47 |
48 | // Definition describes the `list_issue_comments` tool. It requires `owner`, `repo`,
49 | // and the issue `index`. It supports time-based filtering and pagination and is
50 | // marked as a safe, read-only operation.
51 | func (ListIssueCommentsImpl) Definition() *mcp.Tool {
52 | return &mcp.Tool{
53 | Name: "list_issue_comments",
54 | Title: "List Issue Comments",
55 | Description: "List all comments on a specific issue, including comment body, author, and timestamps.",
56 | Annotations: &mcp.ToolAnnotations{
57 | ReadOnlyHint: true,
58 | IdempotentHint: true,
59 | },
60 | InputSchema: &jsonschema.Schema{
61 | Type: "object",
62 | Properties: map[string]*jsonschema.Schema{
63 | "owner": {
64 | Type: "string",
65 | Description: "Repository owner (username or organization name)",
66 | },
67 | "repo": {
68 | Type: "string",
69 | Description: "Repository name",
70 | },
71 | "index": {
72 | Type: "integer",
73 | Description: "Issue index number",
74 | },
75 | "since": {
76 | Type: "string",
77 | Description: "Only show comments updated after this time (ISO 8601 format) (optional)",
78 | Format: "date-time",
79 | },
80 | "before": {
81 | Type: "string",
82 | Description: "Only show comments updated before this time (ISO 8601 format) (optional)",
83 | Format: "date-time",
84 | },
85 | "page": {
86 | Type: "integer",
87 | Description: "Page number for pagination (optional, defaults to 1)",
88 | Minimum: tools.Float64Ptr(1),
89 | },
90 | "limit": {
91 | Type: "integer",
92 | Description: "Number of comments per page (optional, defaults to 20, max 50)",
93 | Minimum: tools.Float64Ptr(1),
94 | Maximum: tools.Float64Ptr(50),
95 | },
96 | },
97 | Required: []string{"owner", "repo", "index"},
98 | },
99 | }
100 | }
101 |
102 | // Handler implements the logic for listing issue comments. It calls the Forgejo SDK's
103 | // `ListIssueComments` function and formats the results into a markdown list.
104 | func (impl ListIssueCommentsImpl) Handler() mcp.ToolHandlerFor[ListIssueCommentsParams, any] {
105 | return func(ctx context.Context, req *mcp.CallToolRequest, args ListIssueCommentsParams) (*mcp.CallToolResult, any, error) {
106 | p := args
107 |
108 | opt := forgejo.ListIssueCommentOptions{}
109 | if !p.Since.IsZero() {
110 | opt.Since = p.Since
111 | }
112 | if !p.Before.IsZero() {
113 | opt.Before = p.Before
114 | }
115 | if p.Page > 0 {
116 | opt.Page = p.Page
117 | }
118 | if p.Limit > 0 {
119 | opt.PageSize = p.Limit
120 | }
121 |
122 | comments, _, err := impl.Client.ListIssueComments(p.Owner, p.Repo, int64(p.Index), opt)
123 | if err != nil {
124 | return nil, nil, fmt.Errorf("failed to list comments: %w", err)
125 | }
126 |
127 | var content string
128 | if len(comments) == 0 {
129 | content = "No comments found for this issue."
130 | } else {
131 | var commentsMarkdown string
132 | for _, comment := range comments {
133 | commentWrapper := &types.Comment{Comment: comment}
134 | commentsMarkdown += commentWrapper.ToMarkdown() + "\n\n---\n\n"
135 | }
136 | content = fmt.Sprintf("Found %d comments\n\n%s", len(comments), commentsMarkdown)
137 | }
138 |
139 | return &mcp.CallToolResult{
140 | Content: []mcp.Content{
141 | &mcp.TextContent{
142 | Text: content,
143 | },
144 | },
145 | }, nil, nil
146 | }
147 | }
148 |
149 | // CreateIssueCommentParams defines the parameters for the create_issue_comment tool.
150 | // It specifies the issue to comment on and the content of the comment.
151 | type CreateIssueCommentParams struct {
152 | // Owner is the username or organization name that owns the repository.
153 | Owner string `json:"owner"`
154 | // Repo is the name of the repository.
155 | Repo string `json:"repo"`
156 | // Index is the issue number.
157 | Index int `json:"index"`
158 | // Body is the markdown content of the comment.
159 | Body string `json:"body"`
160 | }
161 |
162 | // CreateIssueCommentImpl implements the MCP tool for creating a new comment on an issue.
163 | // This is a non-idempotent operation that posts a new comment using the Forgejo SDK.
164 | type CreateIssueCommentImpl struct {
165 | Client *tools.Client
166 | }
167 |
168 | // Definition describes the `create_issue_comment` tool. It requires the issue `index`
169 | // and the comment `body`. It is not idempotent, as multiple calls will create
170 | // multiple identical comments.
171 | func (CreateIssueCommentImpl) Definition() *mcp.Tool {
172 | return &mcp.Tool{
173 | Name: "create_issue_comment",
174 | Title: "Add Issue Comment",
175 | Description: "Add a comment to an existing issue.",
176 | Annotations: &mcp.ToolAnnotations{
177 | ReadOnlyHint: false,
178 | DestructiveHint: tools.BoolPtr(false),
179 | IdempotentHint: false,
180 | },
181 | InputSchema: &jsonschema.Schema{
182 | Type: "object",
183 | Properties: map[string]*jsonschema.Schema{
184 | "owner": {
185 | Type: "string",
186 | Description: "Repository owner (username or organization name)",
187 | },
188 | "repo": {
189 | Type: "string",
190 | Description: "Repository name",
191 | },
192 | "index": {
193 | Type: "integer",
194 | Description: "Issue index number",
195 | },
196 | "body": {
197 | Type: "string",
198 | Description: "Comment body content (markdown supported)",
199 | },
200 | },
201 | Required: []string{"owner", "repo", "index", "body"},
202 | },
203 | }
204 | }
205 |
206 | // Handler implements the logic for creating an issue comment. It calls the Forgejo
207 | // SDK's `CreateIssueComment` function and returns the details of the new comment.
208 | func (impl CreateIssueCommentImpl) Handler() mcp.ToolHandlerFor[CreateIssueCommentParams, any] {
209 | return func(ctx context.Context, req *mcp.CallToolRequest, args CreateIssueCommentParams) (*mcp.CallToolResult, any, error) {
210 | p := args
211 |
212 | opt := forgejo.CreateIssueCommentOption{
213 | Body: p.Body,
214 | }
215 |
216 | comment, _, err := impl.Client.CreateIssueComment(p.Owner, p.Repo, int64(p.Index), opt)
217 | if err != nil {
218 | return nil, nil, fmt.Errorf("failed to create comment: %w", err)
219 | }
220 |
221 | // reply the id of the comment
222 | return &mcp.CallToolResult{
223 | Content: []mcp.Content{
224 | &mcp.TextContent{
225 | Text: fmt.Sprintf("Comment#%d has been created successfully.", comment.ID),
226 | },
227 | },
228 | }, nil, nil
229 | }
230 | }
231 |
232 | // EditIssueCommentParams defines the parameters for the edit_issue_comment tool.
233 | // It specifies the comment to edit by its ID and the new content.
234 | type EditIssueCommentParams struct {
235 | // Owner is the username or organization name that owns the repository.
236 | Owner string `json:"owner"`
237 | // Repo is the name of the repository.
238 | Repo string `json:"repo"`
239 | // CommentID is the unique identifier of the comment to edit.
240 | CommentID int `json:"comment_id"`
241 | // Body is the new markdown content for the comment.
242 | Body string `json:"body"`
243 | }
244 |
245 | // EditIssueCommentImpl implements the MCP tool for editing an existing issue comment.
246 | // This is an idempotent operation that modifies a comment's content using the
247 | // Forgejo SDK.
248 | type EditIssueCommentImpl struct {
249 | Client *tools.Client
250 | }
251 |
252 | // Definition describes the `edit_issue_comment` tool. It requires the `comment_id`
253 | // and the new `body`. It is marked as idempotent.
254 | func (EditIssueCommentImpl) Definition() *mcp.Tool {
255 | return &mcp.Tool{
256 | Name: "edit_issue_comment",
257 | Title: "Edit Issue Comment",
258 | Description: "Edit an existing comment on an issue. You can modify the comment body content.",
259 | Annotations: &mcp.ToolAnnotations{
260 | ReadOnlyHint: false,
261 | DestructiveHint: tools.BoolPtr(false),
262 | IdempotentHint: true,
263 | },
264 | InputSchema: &jsonschema.Schema{
265 | Type: "object",
266 | Properties: map[string]*jsonschema.Schema{
267 | "owner": {
268 | Type: "string",
269 | Description: "Repository owner (username or organization name)",
270 | },
271 | "repo": {
272 | Type: "string",
273 | Description: "Repository name",
274 | },
275 | "comment_id": {
276 | Type: "integer",
277 | Description: "Comment ID to edit",
278 | },
279 | "body": {
280 | Type: "string",
281 | Description: "New comment body content (markdown supported)",
282 | },
283 | },
284 | Required: []string{"owner", "repo", "comment_id", "body"},
285 | },
286 | }
287 | }
288 |
289 | // Handler implements the logic for editing an issue comment. It calls the Forgejo
290 | // SDK's `EditIssueComment` function. It will return an error if the comment ID
291 | // is not found.
292 | func (impl EditIssueCommentImpl) Handler() mcp.ToolHandlerFor[EditIssueCommentParams, any] {
293 | return func(ctx context.Context, req *mcp.CallToolRequest, args EditIssueCommentParams) (*mcp.CallToolResult, any, error) {
294 | p := args
295 |
296 | opt := forgejo.EditIssueCommentOption{
297 | Body: p.Body,
298 | }
299 |
300 | comment, _, err := impl.Client.EditIssueComment(p.Owner, p.Repo, int64(p.CommentID), opt)
301 | if err != nil {
302 | return nil, nil, fmt.Errorf("failed to edit comment: %w", err)
303 | }
304 |
305 | commentWrapper := &types.Comment{Comment: comment}
306 |
307 | return &mcp.CallToolResult{
308 | Content: []mcp.Content{
309 | &mcp.TextContent{
310 | Text: commentWrapper.ToMarkdown(),
311 | },
312 | },
313 | }, nil, nil
314 | }
315 | }
316 |
317 | // DeleteIssueCommentParams defines the parameters for the delete_issue_comment tool.
318 | // It specifies the comment to be deleted by its ID.
319 | type DeleteIssueCommentParams struct {
320 | // Owner is the username or organization name that owns the repository.
321 | Owner string `json:"owner"`
322 | // Repo is the name of the repository.
323 | Repo string `json:"repo"`
324 | // CommentID is the unique identifier of the comment to delete.
325 | CommentID int `json:"comment_id"`
326 | }
327 |
328 | // DeleteIssueCommentImpl implements the destructive MCP tool for deleting an issue comment.
329 | // This is an idempotent but irreversible operation that removes a comment using the
330 | // Forgejo SDK.
331 | type DeleteIssueCommentImpl struct {
332 | Client *tools.Client
333 | }
334 |
335 | // Definition describes the `delete_issue_comment` tool. It requires the `comment_id`
336 | // to be deleted. It is marked as a destructive operation to ensure clients can
337 | // warn the user before execution.
338 | func (DeleteIssueCommentImpl) Definition() *mcp.Tool {
339 | return &mcp.Tool{
340 | Name: "delete_issue_comment",
341 | Title: "Delete Issue Comment",
342 | Description: "Delete a specific comment from an issue. This action cannot be undone.",
343 | Annotations: &mcp.ToolAnnotations{
344 | ReadOnlyHint: false,
345 | DestructiveHint: tools.BoolPtr(true),
346 | IdempotentHint: true,
347 | },
348 | InputSchema: &jsonschema.Schema{
349 | Type: "object",
350 | Properties: map[string]*jsonschema.Schema{
351 | "owner": {
352 | Type: "string",
353 | Description: "Repository owner (username or organization name)",
354 | },
355 | "repo": {
356 | Type: "string",
357 | Description: "Repository name",
358 | },
359 | "comment_id": {
360 | Type: "integer",
361 | Description: "Comment ID to delete",
362 | },
363 | },
364 | Required: []string{"owner", "repo", "comment_id"},
365 | },
366 | }
367 | }
368 |
369 | // Handler implements the logic for deleting an issue comment. It calls the Forgejo
370 | // SDK's `DeleteIssueComment` function. On success, it returns a simple text
371 | // confirmation. It will return an error if the comment does not exist.
372 | func (impl DeleteIssueCommentImpl) Handler() mcp.ToolHandlerFor[DeleteIssueCommentParams, any] {
373 | return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteIssueCommentParams) (*mcp.CallToolResult, any, error) {
374 | p := args
375 |
376 | _, err := impl.Client.DeleteIssueComment(p.Owner, p.Repo, int64(p.CommentID))
377 | if err != nil {
378 | return nil, nil, fmt.Errorf("failed to delete comment: %w", err)
379 | }
380 |
381 | return &mcp.CallToolResult{
382 | Content: []mcp.Content{
383 | &mcp.TextContent{
384 | Text: fmt.Sprintf("Comment %d successfully deleted.", p.CommentID),
385 | },
386 | },
387 | }, nil, nil
388 | }
389 | }
390 |
```
--------------------------------------------------------------------------------
/tools/milestone/crud.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 milestone
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 | // ListRepoMilestonesParams defines the parameters for the list_repo_milestones tool.
23 | // It specifies the repository and an optional state filter.
24 | type ListRepoMilestonesParams 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 | // State filters milestones by their state (e.g., 'open', 'closed').
30 | State string `json:"state,omitempty"`
31 | }
32 |
33 | // ListRepoMilestonesImpl implements the read-only MCP tool for listing repository milestones.
34 | // This is a safe, idempotent operation that uses the Forgejo SDK to fetch a list
35 | // of milestones, optionally filtered by their state.
36 | type ListRepoMilestonesImpl struct {
37 | Client *tools.Client
38 | }
39 |
40 | // Definition describes the `list_repo_milestones` tool. It requires `owner` and `repo`,
41 | // supports an optional `state` filter, and is marked as a safe, read-only operation.
42 | func (ListRepoMilestonesImpl) Definition() *mcp.Tool {
43 | return &mcp.Tool{
44 | Name: "list_repo_milestones",
45 | Title: "List Repository Milestones",
46 | Description: "List all milestones in a repository. Returns milestone information including title, description, due date, and status.",
47 | Annotations: &mcp.ToolAnnotations{
48 | ReadOnlyHint: true,
49 | IdempotentHint: true,
50 | },
51 | InputSchema: &jsonschema.Schema{
52 | Type: "object",
53 | Properties: map[string]*jsonschema.Schema{
54 | "owner": {
55 | Type: "string",
56 | Description: "Repository owner (username or organization name)",
57 | },
58 | "repo": {
59 | Type: "string",
60 | Description: "Repository name",
61 | },
62 | "state": {
63 | Type: "string",
64 | Description: "Milestone state filter: 'open', 'closed', or 'all' (optional, defaults to 'open')",
65 | Enum: []any{"open", "closed", "all"},
66 | },
67 | },
68 | Required: []string{"owner", "repo"},
69 | },
70 | }
71 | }
72 |
73 | // Handler implements the logic for listing milestones. It calls the Forgejo SDK's
74 | // `ListRepoMilestones` function and formats the results into a markdown list.
75 | func (impl ListRepoMilestonesImpl) Handler() mcp.ToolHandlerFor[ListRepoMilestonesParams, any] {
76 | return func(ctx context.Context, req *mcp.CallToolRequest, args ListRepoMilestonesParams) (*mcp.CallToolResult, any, error) {
77 | p := args
78 |
79 | // Build options for SDK call
80 | opt := forgejo.ListMilestoneOption{}
81 | if p.State != "" {
82 | opt.State = forgejo.StateType(p.State)
83 | }
84 |
85 | // Call SDK
86 | milestones, _, err := impl.Client.ListRepoMilestones(p.Owner, p.Repo, opt)
87 | if err != nil {
88 | return nil, nil, fmt.Errorf("failed to list milestones: %w", err)
89 | }
90 |
91 | // Convert to our types and format
92 | var content string
93 | if len(milestones) == 0 {
94 | content = "No milestones found in this repository."
95 | } else {
96 | // Convert milestones to our type
97 | milestoneList := make(types.MilestoneList, len(milestones))
98 | for i, milestone := range milestones {
99 | milestoneList[i] = &types.Milestone{Milestone: milestone}
100 | }
101 |
102 | content = fmt.Sprintf("Found %d milestones\n\n%s",
103 | len(milestones), milestoneList.ToMarkdown())
104 | }
105 |
106 | return &mcp.CallToolResult{
107 | Content: []mcp.Content{
108 | &mcp.TextContent{
109 | Text: content,
110 | },
111 | },
112 | }, nil, nil
113 | }
114 | }
115 |
116 | // CreateMilestoneParams defines the parameters for the create_milestone tool.
117 | // It includes the title and optional description and due date.
118 | type CreateMilestoneParams struct {
119 | // Owner is the username or organization name that owns the repository.
120 | Owner string `json:"owner"`
121 | // Repo is the name of the repository.
122 | Repo string `json:"repo"`
123 | // Title is the title of the new milestone.
124 | Title string `json:"title"`
125 | // Description is the markdown description of the milestone.
126 | Description string `json:"description,omitempty"`
127 | // DueDate is the optional due date for the milestone.
128 | DueDate time.Time `json:"due_date,omitempty"`
129 | }
130 |
131 | // CreateMilestoneImpl implements the MCP tool for creating a new milestone.
132 | // This is a non-idempotent operation that creates a new milestone object
133 | // using the Forgejo SDK.
134 | type CreateMilestoneImpl struct {
135 | Client *tools.Client
136 | }
137 |
138 | // Definition describes the `create_milestone` tool. It requires `owner`, `repo`,
139 | // and a `title`. It is not idempotent, as multiple calls with the same title
140 | // will create multiple milestones.
141 | func (CreateMilestoneImpl) Definition() *mcp.Tool {
142 | return &mcp.Tool{
143 | Name: "create_milestone",
144 | Title: "Create Milestone",
145 | Description: "Create a new milestone in a repository. Specify the title, description, and optional due date.",
146 | Annotations: &mcp.ToolAnnotations{
147 | ReadOnlyHint: false,
148 | DestructiveHint: tools.BoolPtr(false),
149 | IdempotentHint: false,
150 | },
151 | InputSchema: &jsonschema.Schema{
152 | Type: "object",
153 | Properties: map[string]*jsonschema.Schema{
154 | "owner": {
155 | Type: "string",
156 | Description: "Repository owner (username or organization name)",
157 | },
158 | "repo": {
159 | Type: "string",
160 | Description: "Repository name",
161 | },
162 | "title": {
163 | Type: "string",
164 | Description: "Milestone title",
165 | },
166 | "description": {
167 | Type: "string",
168 | Description: "Milestone description (optional)",
169 | },
170 | "due_date": {
171 | Type: "string",
172 | Description: "Milestone due date in ISO 8601 format (e.g., '2024-12-31T23:59:59Z') (optional)",
173 | Format: "date-time",
174 | },
175 | },
176 | Required: []string{"owner", "repo", "title"},
177 | },
178 | }
179 | }
180 |
181 | // Handler implements the logic for creating a milestone. It calls the Forgejo SDK's
182 | // `CreateMilestone` function and returns the details of the newly created milestone.
183 | func (impl CreateMilestoneImpl) Handler() mcp.ToolHandlerFor[CreateMilestoneParams, any] {
184 | return func(ctx context.Context, req *mcp.CallToolRequest, args CreateMilestoneParams) (*mcp.CallToolResult, any, error) {
185 | p := args
186 |
187 | // Build options for SDK call
188 | opt := forgejo.CreateMilestoneOption{
189 | Title: p.Title,
190 | Description: p.Description,
191 | }
192 |
193 | // Set due date if provided
194 | if !p.DueDate.IsZero() {
195 | opt.Deadline = &p.DueDate
196 | }
197 |
198 | // Call SDK
199 | milestone, _, err := impl.Client.CreateMilestone(p.Owner, p.Repo, opt)
200 | if err != nil {
201 | return nil, nil, fmt.Errorf("failed to create milestone: %w", err)
202 | }
203 |
204 | // Convert to our type and format
205 | milestoneWrapper := &types.Milestone{Milestone: milestone}
206 |
207 | return &mcp.CallToolResult{
208 | Content: []mcp.Content{
209 | &mcp.TextContent{
210 | Text: milestoneWrapper.ToMarkdown(),
211 | },
212 | },
213 | }, nil, nil
214 | }
215 | }
216 |
217 | // EditMilestoneParams defines the parameters for the edit_milestone tool.
218 | // It specifies the milestone to edit by ID and the fields to update.
219 | type EditMilestoneParams struct {
220 | // Owner is the username or organization name that owns the repository.
221 | Owner string `json:"owner"`
222 | // Repo is the name of the repository.
223 | Repo string `json:"repo"`
224 | // ID is the unique identifier of the milestone to edit.
225 | ID int `json:"id"`
226 | // Title is the new title for the milestone.
227 | Title string `json:"title,omitempty"`
228 | // Description is the new markdown description for the milestone.
229 | Description string `json:"description,omitempty"`
230 | // DueDate is the new optional due date for the milestone.
231 | DueDate time.Time `json:"due_date,omitempty"`
232 | // State is the new state for the milestone (e.g., 'open', 'closed').
233 | State string `json:"state,omitempty"`
234 | }
235 |
236 | // EditMilestoneImpl implements the MCP tool for editing an existing milestone.
237 | // This is an idempotent operation that modifies a milestone's metadata using
238 | // the Forgejo SDK.
239 | type EditMilestoneImpl struct {
240 | Client *tools.Client
241 | }
242 |
243 | // Definition describes the `edit_milestone` tool. It requires `owner`, `repo`, and
244 | // the milestone `id`. It is marked as idempotent.
245 | func (EditMilestoneImpl) Definition() *mcp.Tool {
246 | return &mcp.Tool{
247 | Name: "edit_milestone",
248 | Title: "Edit Milestone",
249 | Description: "Edit an existing milestone's title, description, due date, or state.",
250 | Annotations: &mcp.ToolAnnotations{
251 | ReadOnlyHint: false,
252 | DestructiveHint: tools.BoolPtr(false),
253 | IdempotentHint: true,
254 | },
255 | InputSchema: &jsonschema.Schema{
256 | Type: "object",
257 | Properties: map[string]*jsonschema.Schema{
258 | "owner": {
259 | Type: "string",
260 | Description: "Repository owner (username or organization name)",
261 | },
262 | "repo": {
263 | Type: "string",
264 | Description: "Repository name",
265 | },
266 | "id": {
267 | Type: "integer",
268 | Description: "Milestone ID",
269 | },
270 | "title": {
271 | Type: "string",
272 | Description: "New milestone title (optional)",
273 | },
274 | "description": {
275 | Type: "string",
276 | Description: "New milestone description (optional)",
277 | },
278 | "due_date": {
279 | Type: "string",
280 | Description: "New milestone due date in ISO 8601 format (e.g., '2024-12-31T23:59:59Z') (optional)",
281 | Format: "date-time",
282 | },
283 | "state": {
284 | Type: "string",
285 | Description: "New milestone state: 'open' or 'closed' (optional)",
286 | Enum: []any{"open", "closed"},
287 | },
288 | },
289 | Required: []string{"owner", "repo", "id"},
290 | },
291 | }
292 | }
293 |
294 | // Handler implements the logic for editing a milestone. It calls the Forgejo SDK's
295 | // `EditMilestone` function. It will return an error if the milestone ID is not found.
296 | func (impl EditMilestoneImpl) Handler() mcp.ToolHandlerFor[EditMilestoneParams, any] {
297 | return func(ctx context.Context, req *mcp.CallToolRequest, args EditMilestoneParams) (*mcp.CallToolResult, any, error) {
298 | p := args
299 |
300 | // Build options for SDK call
301 | opt := forgejo.EditMilestoneOption{}
302 | if p.Title != "" {
303 | opt.Title = p.Title
304 | }
305 | if p.Description != "" {
306 | opt.Description = &p.Description
307 | }
308 | if p.State != "" {
309 | state := forgejo.StateType(p.State)
310 | opt.State = &state
311 | }
312 | if !p.DueDate.IsZero() {
313 | opt.Deadline = &p.DueDate
314 | }
315 |
316 | // Call SDK
317 | milestone, _, err := impl.Client.EditMilestone(p.Owner, p.Repo, int64(p.ID), opt)
318 | if err != nil {
319 | return nil, nil, fmt.Errorf("failed to edit milestone: %w", err)
320 | }
321 |
322 | // Convert to our type and format
323 | milestoneWrapper := &types.Milestone{Milestone: milestone}
324 |
325 | return &mcp.CallToolResult{
326 | Content: []mcp.Content{
327 | &mcp.TextContent{
328 | Text: milestoneWrapper.ToMarkdown(),
329 | },
330 | },
331 | }, nil, nil
332 | }
333 | }
334 |
335 | // DeleteMilestoneParams defines the parameters for the delete_milestone tool.
336 | // It specifies the milestone to be deleted by its ID.
337 | type DeleteMilestoneParams struct {
338 | // Owner is the username or organization name that owns the repository.
339 | Owner string `json:"owner"`
340 | // Repo is the name of the repository.
341 | Repo string `json:"repo"`
342 | // ID is the unique identifier of the milestone to delete.
343 | ID int `json:"id"`
344 | }
345 |
346 | // DeleteMilestoneImpl implements the destructive MCP tool for deleting a milestone.
347 | // This is an idempotent but irreversible operation that removes a milestone from
348 | // a repository using the Forgejo SDK.
349 | type DeleteMilestoneImpl struct {
350 | Client *tools.Client
351 | }
352 |
353 | // Definition describes the `delete_milestone` tool. It requires `owner`, `repo`,
354 | // and the milestone `id`. It is marked as a destructive operation to ensure
355 | // clients can warn the user before execution.
356 | func (DeleteMilestoneImpl) Definition() *mcp.Tool {
357 | return &mcp.Tool{
358 | Name: "delete_milestone",
359 | Title: "Delete Milestone",
360 | Description: "Delete a milestone from a repository.",
361 | Annotations: &mcp.ToolAnnotations{
362 | ReadOnlyHint: false,
363 | DestructiveHint: tools.BoolPtr(true),
364 | IdempotentHint: true,
365 | },
366 | InputSchema: &jsonschema.Schema{
367 | Type: "object",
368 | Properties: map[string]*jsonschema.Schema{
369 | "owner": {
370 | Type: "string",
371 | Description: "Repository owner (username or organization name)",
372 | },
373 | "repo": {
374 | Type: "string",
375 | Description: "Repository name",
376 | },
377 | "id": {
378 | Type: "integer",
379 | Description: "Milestone ID to delete",
380 | },
381 | },
382 | Required: []string{"owner", "repo", "id"},
383 | },
384 | }
385 | }
386 |
387 | // Handler implements the logic for deleting a milestone. It calls the Forgejo SDK's
388 | // `DeleteMilestone` function. On success, it returns a simple text confirmation.
389 | // It will return an error if the milestone does not exist.
390 | func (impl DeleteMilestoneImpl) Handler() mcp.ToolHandlerFor[DeleteMilestoneParams, any] {
391 | return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteMilestoneParams) (*mcp.CallToolResult, any, error) {
392 | p := args
393 |
394 | // Call SDK
395 | _, err := impl.Client.DeleteMilestone(p.Owner, p.Repo, int64(p.ID))
396 | if err != nil {
397 | return nil, nil, fmt.Errorf("failed to delete milestone: %w", err)
398 | }
399 |
400 | // Return success message
401 | emptyResponse := types.EmptyResponse{}
402 |
403 | return &mcp.CallToolResult{
404 | Content: []mcp.Content{
405 | &mcp.TextContent{
406 | Text: emptyResponse.ToMarkdown(),
407 | },
408 | },
409 | }, nil, nil
410 | }
411 | }
412 |
```
--------------------------------------------------------------------------------
/tools/release/crud.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 release
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 | // ListReleasesParams defines the parameters for the list_releases tool.
22 | // It specifies the owner and repository, with optional pagination.
23 | type ListReleasesParams 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 | // Page is the page number for pagination.
29 | Page int `json:"page,omitempty"`
30 | // Limit is the number of releases to return per page.
31 | Limit int `json:"limit,omitempty"`
32 | }
33 |
34 | // ListReleasesImpl implements the read-only MCP tool for listing repository releases.
35 | // This is a safe, idempotent operation that uses the Forgejo SDK to fetch a
36 | // paginated list of releases.
37 | type ListReleasesImpl struct {
38 | Client *tools.Client
39 | }
40 |
41 | // Definition describes the `list_releases` tool. It requires `owner` and `repo`
42 | // and supports pagination. It is marked as a safe, read-only operation.
43 | func (ListReleasesImpl) Definition() *mcp.Tool {
44 | return &mcp.Tool{
45 | Name: "list_releases",
46 | Title: "List Releases",
47 | Description: "List all releases in a repository. Returns release information including tag name, title, description, and assets.",
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 releases per page (optional, defaults to 10, 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 releases. It calls the Forgejo SDK's
81 | // `ListReleases` function and formats the results into a markdown list.
82 | func (impl ListReleasesImpl) Handler() mcp.ToolHandlerFor[ListReleasesParams, any] {
83 | return func(ctx context.Context, req *mcp.CallToolRequest, args ListReleasesParams) (*mcp.CallToolResult, any, error) {
84 | p := args
85 |
86 | // Build options for SDK call
87 | opt := forgejo.ListReleasesOptions{}
88 | if p.Page > 0 {
89 | opt.Page = p.Page
90 | }
91 | if p.Limit > 0 {
92 | opt.PageSize = p.Limit
93 | }
94 |
95 | // Call SDK
96 | releases, _, err := impl.Client.ListReleases(p.Owner, p.Repo, opt)
97 | if err != nil {
98 | return nil, nil, fmt.Errorf("failed to list releases: %w", err)
99 | }
100 |
101 | // Convert to our types and format
102 | var content string
103 | if len(releases) == 0 {
104 | content = "No releases found in this repository."
105 | } else {
106 | // Convert releases to our type
107 | releaseList := make(types.ReleaseList, len(releases))
108 | for i, release := range releases {
109 | releaseList[i] = &types.Release{Release: release}
110 | }
111 |
112 | content = fmt.Sprintf("Found %d releases\n\n%s",
113 | len(releases), releaseList.ToMarkdown())
114 | }
115 |
116 | return &mcp.CallToolResult{
117 | Content: []mcp.Content{
118 | &mcp.TextContent{
119 | Text: content,
120 | },
121 | },
122 | }, nil, nil
123 | }
124 | }
125 |
126 | // CreateReleaseParams defines the parameters for the create_release tool.
127 | // It includes all necessary details for creating a new release.
128 | type CreateReleaseParams struct {
129 | // Owner is the username or organization name that owns the repository.
130 | Owner string `json:"owner"`
131 | // Repo is the name of the repository.
132 | Repo string `json:"repo"`
133 | // TagName is the name of the git tag for the release.
134 | TagName string `json:"tag_name"`
135 | // TargetCommitish is the branch or commit SHA to create the release from.
136 | TargetCommitish string `json:"target_commitish,omitempty"`
137 | // Name is the title of the release.
138 | Name string `json:"name"`
139 | // Body is the markdown description of the release.
140 | Body string `json:"body,omitempty"`
141 | // Draft indicates whether the release is a draft.
142 | Draft bool `json:"draft,omitempty"`
143 | // Prerelease indicates whether the release is a pre-release.
144 | Prerelease bool `json:"prerelease,omitempty"`
145 | }
146 |
147 | // CreateReleaseImpl implements the MCP tool for creating a new release.
148 | // This is a non-idempotent operation that creates a new git tag and a
149 | // corresponding release object via the Forgejo SDK.
150 | type CreateReleaseImpl struct {
151 | Client *tools.Client
152 | }
153 |
154 | // Definition describes the `create_release` tool. It requires `owner`, `repo`,
155 | // `tag_name`, and a `name`. It is not idempotent, as multiple calls will fail
156 | // once the tag is created.
157 | func (CreateReleaseImpl) Definition() *mcp.Tool {
158 | return &mcp.Tool{
159 | Name: "create_release",
160 | Title: "Create Release",
161 | Description: "Create a new release in a repository. Specify tag name, title, description, and other metadata.",
162 | Annotations: &mcp.ToolAnnotations{
163 | ReadOnlyHint: false,
164 | DestructiveHint: tools.BoolPtr(false),
165 | IdempotentHint: false,
166 | },
167 | InputSchema: &jsonschema.Schema{
168 | Type: "object",
169 | Properties: map[string]*jsonschema.Schema{
170 | "owner": {
171 | Type: "string",
172 | Description: "Repository owner (username or organization name)",
173 | },
174 | "repo": {
175 | Type: "string",
176 | Description: "Repository name",
177 | },
178 | "tag_name": {
179 | Type: "string",
180 | Description: "Git tag name for this release",
181 | },
182 | "target_commitish": {
183 | Type: "string",
184 | Description: "Target branch or commit SHA (optional, defaults to default branch)",
185 | },
186 | "name": {
187 | Type: "string",
188 | Description: "Release title",
189 | },
190 | "body": {
191 | Type: "string",
192 | Description: "Release description (markdown supported) (optional)",
193 | },
194 | "draft": {
195 | Type: "boolean",
196 | Description: "Whether this is a draft release (optional, defaults to false)",
197 | },
198 | "prerelease": {
199 | Type: "boolean",
200 | Description: "Whether this is a prerelease (optional, defaults to false)",
201 | },
202 | },
203 | Required: []string{"owner", "repo", "tag_name", "name"},
204 | },
205 | }
206 | }
207 |
208 | // Handler implements the logic for creating a release. It calls the Forgejo SDK's
209 | // `CreateRelease` function. On success, it returns details of the new release.
210 | func (impl CreateReleaseImpl) Handler() mcp.ToolHandlerFor[CreateReleaseParams, any] {
211 | return func(ctx context.Context, req *mcp.CallToolRequest, args CreateReleaseParams) (*mcp.CallToolResult, any, error) {
212 | p := args
213 |
214 | // Build options for SDK call
215 | opt := forgejo.CreateReleaseOption{
216 | TagName: p.TagName,
217 | Target: p.TargetCommitish,
218 | Title: p.Name,
219 | Note: p.Body,
220 | IsDraft: p.Draft,
221 | IsPrerelease: p.Prerelease,
222 | }
223 |
224 | // Call SDK
225 | release, _, err := impl.Client.CreateRelease(p.Owner, p.Repo, opt)
226 | if err != nil {
227 | return nil, nil, fmt.Errorf("failed to create release: %w", err)
228 | }
229 |
230 | // Convert to our type and format
231 | releaseWrapper := &types.Release{Release: release}
232 |
233 | return &mcp.CallToolResult{
234 | Content: []mcp.Content{
235 | &mcp.TextContent{
236 | Text: releaseWrapper.ToMarkdown(),
237 | },
238 | },
239 | }, nil, nil
240 | }
241 | }
242 |
243 | // EditReleaseParams defines the parameters for the edit_release tool.
244 | // It specifies the release to edit by ID and the fields to update.
245 | type EditReleaseParams struct {
246 | // Owner is the username or organization name that owns the repository.
247 | Owner string `json:"owner"`
248 | // Repo is the name of the repository.
249 | Repo string `json:"repo"`
250 | // ID is the unique identifier of the release to edit.
251 | ID int `json:"id"`
252 | // TagName is the new git tag name for the release.
253 | TagName string `json:"tag_name,omitempty"`
254 | // TargetCommitish is the new target branch or commit SHA.
255 | TargetCommitish string `json:"target_commitish,omitempty"`
256 | // Name is the new title for the release.
257 | Name string `json:"name,omitempty"`
258 | // Body is the new markdown description for the release.
259 | Body string `json:"body,omitempty"`
260 | // Draft indicates whether the release should be marked as a draft.
261 | Draft bool `json:"draft,omitempty"`
262 | // Prerelease indicates whether the release should be marked as a pre-release.
263 | Prerelease bool `json:"prerelease,omitempty"`
264 | }
265 |
266 | // EditReleaseImpl implements the MCP tool for editing an existing release.
267 | // This is an idempotent operation that modifies the metadata of a release
268 | // identified by its ID, using the Forgejo SDK.
269 | type EditReleaseImpl struct {
270 | Client *tools.Client
271 | }
272 |
273 | // Definition describes the `edit_release` tool. It requires `owner`, `repo`, and
274 | // the release `id`. It is marked as idempotent, as multiple identical calls
275 | // will result in the same state.
276 | func (EditReleaseImpl) Definition() *mcp.Tool {
277 | return &mcp.Tool{
278 | Name: "edit_release",
279 | Title: "Edit Release",
280 | Description: "Edit an existing release's title, description, or other metadata.",
281 | Annotations: &mcp.ToolAnnotations{
282 | ReadOnlyHint: false,
283 | DestructiveHint: tools.BoolPtr(false),
284 | IdempotentHint: true,
285 | },
286 | InputSchema: &jsonschema.Schema{
287 | Type: "object",
288 | Properties: map[string]*jsonschema.Schema{
289 | "owner": {
290 | Type: "string",
291 | Description: "Repository owner (username or organization name)",
292 | },
293 | "repo": {
294 | Type: "string",
295 | Description: "Repository name",
296 | },
297 | "id": {
298 | Type: "integer",
299 | Description: "Release ID",
300 | },
301 | "tag_name": {
302 | Type: "string",
303 | Description: "New git tag name for this release (optional)",
304 | },
305 | "target_commitish": {
306 | Type: "string",
307 | Description: "New target branch or commit SHA (optional)",
308 | },
309 | "name": {
310 | Type: "string",
311 | Description: "New release title (optional)",
312 | },
313 | "body": {
314 | Type: "string",
315 | Description: "New release description (markdown supported) (optional)",
316 | },
317 | "draft": {
318 | Type: "boolean",
319 | Description: "Whether this is a draft release (optional)",
320 | },
321 | "prerelease": {
322 | Type: "boolean",
323 | Description: "Whether this is a prerelease (optional)",
324 | },
325 | },
326 | Required: []string{"owner", "repo", "id"},
327 | },
328 | }
329 | }
330 |
331 | // Handler implements the logic for editing a release. It calls the Forgejo SDK's
332 | // `EditRelease` function. It will return an error if the release ID is not found.
333 | func (impl EditReleaseImpl) Handler() mcp.ToolHandlerFor[EditReleaseParams, any] {
334 | return func(ctx context.Context, req *mcp.CallToolRequest, args EditReleaseParams) (*mcp.CallToolResult, any, error) {
335 | p := args
336 |
337 | // Build options for SDK call
338 | opt := forgejo.EditReleaseOption{}
339 | if p.TagName != "" {
340 | opt.TagName = p.TagName
341 | }
342 | if p.TargetCommitish != "" {
343 | opt.Target = p.TargetCommitish
344 | }
345 | if p.Name != "" {
346 | opt.Title = p.Name
347 | }
348 | if p.Body != "" {
349 | opt.Note = p.Body
350 | }
351 | // Note: For boolean fields, we need to check if they were explicitly set
352 | // For now, we'll pass them directly assuming they're properly handled
353 | opt.IsDraft = &p.Draft
354 | opt.IsPrerelease = &p.Prerelease
355 |
356 | // Call SDK
357 | release, _, err := impl.Client.EditRelease(p.Owner, p.Repo, int64(p.ID), opt)
358 | if err != nil {
359 | return nil, nil, fmt.Errorf("failed to edit release: %w", err)
360 | }
361 |
362 | // Convert to our type and format
363 | releaseWrapper := &types.Release{Release: release}
364 |
365 | return &mcp.CallToolResult{
366 | Content: []mcp.Content{
367 | &mcp.TextContent{
368 | Text: releaseWrapper.ToMarkdown(),
369 | },
370 | },
371 | }, nil, nil
372 | }
373 | }
374 |
375 | // DeleteReleaseParams defines the parameters for the delete_release tool.
376 | // It specifies the release to be deleted by its ID.
377 | type DeleteReleaseParams struct {
378 | // Owner is the username or organization name that owns the repository.
379 | Owner string `json:"owner"`
380 | // Repo is the name of the repository.
381 | Repo string `json:"repo"`
382 | // ID is the unique identifier of the release to delete.
383 | ID int `json:"id"`
384 | }
385 |
386 | // DeleteReleaseImpl implements the destructive MCP tool for deleting a release.
387 | // This is an idempotent but irreversible operation that removes both the release
388 | // object and its associated git tag. It uses the Forgejo SDK.
389 | type DeleteReleaseImpl struct {
390 | Client *tools.Client
391 | }
392 |
393 | // Definition describes the `delete_release` tool. It requires `owner`, `repo`,
394 | // and the release `id`. It is marked as a destructive operation to ensure
395 | // clients can warn the user before execution.
396 | func (DeleteReleaseImpl) Definition() *mcp.Tool {
397 | return &mcp.Tool{
398 | Name: "delete_release",
399 | Title: "Delete Release",
400 | Description: "Delete a release from a repository.",
401 | Annotations: &mcp.ToolAnnotations{
402 | ReadOnlyHint: false,
403 | DestructiveHint: tools.BoolPtr(true),
404 | IdempotentHint: true,
405 | },
406 | InputSchema: &jsonschema.Schema{
407 | Type: "object",
408 | Properties: map[string]*jsonschema.Schema{
409 | "owner": {
410 | Type: "string",
411 | Description: "Repository owner (username or organization name)",
412 | },
413 | "repo": {
414 | Type: "string",
415 | Description: "Repository name",
416 | },
417 | "id": {
418 | Type: "integer",
419 | Description: "Release ID to delete",
420 | },
421 | },
422 | Required: []string{"owner", "repo", "id"},
423 | },
424 | }
425 | }
426 |
427 | // Handler implements the logic for deleting a release. It calls the Forgejo SDK's
428 | // `DeleteRelease` function. On success, it returns a simple text confirmation.
429 | // It will return an error if the release does not exist.
430 | func (impl DeleteReleaseImpl) Handler() mcp.ToolHandlerFor[DeleteReleaseParams, any] {
431 | return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteReleaseParams) (*mcp.CallToolResult, any, error) {
432 | p := args
433 |
434 | // Call SDK
435 | _, err := impl.Client.DeleteRelease(p.Owner, p.Repo, int64(p.ID))
436 | if err != nil {
437 | return nil, nil, fmt.Errorf("failed to delete release: %w", err)
438 | }
439 |
440 | // Return success message
441 | emptyResponse := types.EmptyResponse{}
442 |
443 | return &mcp.CallToolResult{
444 | Content: []mcp.Content{
445 | &mcp.TextContent{
446 | Text: emptyResponse.ToMarkdown(),
447 | },
448 | },
449 | }, nil, nil
450 | }
451 | }
452 |
```
--------------------------------------------------------------------------------
/tools/repo/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 repo
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 | // SearchRepositoriesParams defines the parameters for the search_repositories tool.
22 | // It includes the search query and various options for filtering and sorting.
23 | type SearchRepositoriesParams struct {
24 | // Q is the search query string.
25 | Q string `json:"q"`
26 | // Topic indicates whether to search in repository topics.
27 | Topic bool `json:"topic,omitempty"`
28 | // IncludeDesc indicates whether to include repository descriptions in the search.
29 | IncludeDesc bool `json:"include_desc,omitempty"`
30 | // Sort specifies the sort order for the results.
31 | Sort string `json:"sort,omitempty"`
32 | // Order specifies the sort direction (asc or desc).
33 | Order string `json:"order,omitempty"`
34 | // Page is the page number for pagination.
35 | Page int `json:"page,omitempty"`
36 | // Limit is the number of repositories to return per page.
37 | Limit int `json:"limit,omitempty"`
38 | }
39 |
40 | // SearchRepositoriesImpl implements the read-only MCP tool for searching repositories.
41 | // This operation is safe, idempotent, and uses the Forgejo SDK to find repositories
42 | // across the entire Forgejo instance based on a query string.
43 | type SearchRepositoriesImpl struct {
44 | Client *tools.Client
45 | }
46 |
47 | // Definition describes the `search_repositories` tool. It requires a search query `q`
48 | // and supports various optional parameters for sorting and pagination. It is
49 | // marked as a safe, read-only operation.
50 | func (SearchRepositoriesImpl) Definition() *mcp.Tool {
51 | return &mcp.Tool{
52 | Name: "search_repositories",
53 | Title: "Search Repositories",
54 | Description: "Search for repositories across the Forgejo instance. Returns repository information including name, description, and metadata.",
55 | Annotations: &mcp.ToolAnnotations{
56 | ReadOnlyHint: true,
57 | IdempotentHint: true,
58 | },
59 | InputSchema: &jsonschema.Schema{
60 | Type: "object",
61 | Properties: map[string]*jsonschema.Schema{
62 | "q": {
63 | Type: "string",
64 | Description: "Search query string",
65 | },
66 | "topic": {
67 | Type: "boolean",
68 | Description: "Whether to search in repository topics (optional, defaults to true)",
69 | },
70 | "include_desc": {
71 | Type: "boolean",
72 | Description: "Whether to include repository descriptions in search (optional, defaults to true)",
73 | },
74 | "sort": {
75 | Type: "string",
76 | Description: "Sort order: 'alpha', 'created', 'updated', 'size', 'id' (optional, defaults to 'alpha')",
77 | Enum: []any{"alpha", "created", "updated", "size", "id"},
78 | },
79 | "order": {
80 | Type: "string",
81 | Description: "Sort direction: 'asc' or 'desc' (optional, defaults to 'asc')",
82 | Enum: []any{"asc", "desc"},
83 | },
84 | "page": {
85 | Type: "integer",
86 | Description: "Page number for pagination (optional, defaults to 1)",
87 | Minimum: tools.Float64Ptr(1),
88 | },
89 | "limit": {
90 | Type: "integer",
91 | Description: "Number of repositories per page (optional, defaults to 10, max 50)",
92 | Minimum: tools.Float64Ptr(1),
93 | Maximum: tools.Float64Ptr(50),
94 | },
95 | },
96 | Required: []string{"q"},
97 | },
98 | }
99 | }
100 |
101 | // Handler implements the logic for searching repositories. It calls the Forgejo SDK's
102 | // `SearchRepos` function and formats the results into a markdown list.
103 | func (impl SearchRepositoriesImpl) Handler() mcp.ToolHandlerFor[SearchRepositoriesParams, any] {
104 | return func(ctx context.Context, req *mcp.CallToolRequest, args SearchRepositoriesParams) (*mcp.CallToolResult, any, error) {
105 | p := args
106 |
107 | // Build options for SDK call
108 | opt := forgejo.SearchRepoOptions{
109 | Keyword: p.Q,
110 | }
111 | if p.Topic {
112 | opt.KeywordIsTopic = p.Topic
113 | }
114 | if p.IncludeDesc {
115 | opt.KeywordInDescription = p.IncludeDesc
116 | }
117 | if p.Sort != "" {
118 | opt.Sort = p.Sort
119 | }
120 | if p.Order != "" {
121 | opt.Order = p.Order
122 | }
123 | if p.Page > 0 {
124 | opt.Page = p.Page
125 | }
126 | if p.Limit > 0 {
127 | opt.PageSize = p.Limit
128 | }
129 |
130 | // Call SDK
131 | repos, _, err := impl.Client.SearchRepos(opt)
132 | if err != nil {
133 | return nil, nil, fmt.Errorf("failed to search repositories: %w", err)
134 | }
135 |
136 | // Convert to our types and format
137 | var content string
138 | if len(repos) == 0 {
139 | content = "No repositories found matching the search criteria."
140 | } else {
141 | // Convert repos to our type
142 | repoList := make(types.RepositoryList, len(repos))
143 | for i, repo := range repos {
144 | repoList[i] = &types.Repository{Repository: repo}
145 | }
146 |
147 | content = fmt.Sprintf("Found %d repositories\n\n%s",
148 | len(repos), repoList.ToMarkdown())
149 | }
150 |
151 | return &mcp.CallToolResult{
152 | Content: []mcp.Content{
153 | &mcp.TextContent{
154 | Text: content,
155 | },
156 | },
157 | }, nil, nil
158 | }
159 | }
160 |
161 | // ListMyRepositoriesParams defines the parameters for the list_my_repositories tool.
162 | // It allows filtering and sorting of the authenticated user's repositories.
163 | type ListMyRepositoriesParams struct {
164 | // Affiliation filters repositories by the user's role.
165 | Affiliation string `json:"affiliation,omitempty"`
166 | // Visibility filters repositories by their public or private status.
167 | Visibility string `json:"visibility,omitempty"`
168 | // Sort specifies the sort order for the results.
169 | Sort string `json:"sort,omitempty"`
170 | // Direction specifies the sort direction (asc or desc).
171 | Direction string `json:"direction,omitempty"`
172 | // Page is the page number for pagination.
173 | Page int `json:"page,omitempty"`
174 | // Limit is the number of repositories to return per page.
175 | Limit int `json:"limit,omitempty"`
176 | }
177 |
178 | // ListMyRepositoriesImpl implements the read-only MCP tool for listing the
179 | // authenticated user's repositories. This is a safe, idempotent operation that
180 | // uses the Forgejo SDK.
181 | type ListMyRepositoriesImpl struct {
182 | Client *tools.Client
183 | }
184 |
185 | // Definition describes the `list_my_repositories` tool. It supports optional
186 | // parameters for filtering and sorting, and is marked as a safe, read-only operation.
187 | func (ListMyRepositoriesImpl) Definition() *mcp.Tool {
188 | return &mcp.Tool{
189 | Name: "list_my_repositories",
190 | Title: "List My Repositories",
191 | Description: "List repositories owned by the authenticated user. Returns repository information including name, description, and metadata.",
192 | Annotations: &mcp.ToolAnnotations{
193 | ReadOnlyHint: true,
194 | IdempotentHint: true,
195 | },
196 | InputSchema: &jsonschema.Schema{
197 | Type: "object",
198 | Properties: map[string]*jsonschema.Schema{
199 | "affiliation": {
200 | Type: "string",
201 | Description: "Repository affiliation filter: 'owner', 'collaborator', 'organization_member', or 'all' (optional, defaults to 'all')",
202 | Enum: []any{"owner", "collaborator", "organization_member", "all"},
203 | },
204 | "visibility": {
205 | Type: "string",
206 | Description: "Repository visibility filter: 'all', 'public', 'private' (optional, defaults to 'all')",
207 | Enum: []any{"all", "public", "private"},
208 | },
209 | "sort": {
210 | Type: "string",
211 | Description: "Sort order: 'created', 'updated', 'pushed', 'full_name' (optional, defaults to 'full_name')",
212 | Enum: []any{"created", "updated", "pushed", "full_name"},
213 | },
214 | "direction": {
215 | Type: "string",
216 | Description: "Sort direction: 'asc' or 'desc' (optional, defaults to 'asc')",
217 | Enum: []any{"asc", "desc"},
218 | },
219 | "page": {
220 | Type: "integer",
221 | Description: "Page number for pagination (optional, defaults to 1)",
222 | Minimum: tools.Float64Ptr(1),
223 | },
224 | "limit": {
225 | Type: "integer",
226 | Description: "Number of repositories per page (optional, defaults to 20, max 50)",
227 | Minimum: tools.Float64Ptr(1),
228 | Maximum: tools.Float64Ptr(50),
229 | },
230 | },
231 | Required: []string{},
232 | },
233 | }
234 | }
235 |
236 | // Handler implements the logic for listing the user's repositories. It calls the
237 | // Forgejo SDK's `ListMyRepos` function and formats the results into a markdown list.
238 | func (impl ListMyRepositoriesImpl) Handler() mcp.ToolHandlerFor[ListMyRepositoriesParams, any] {
239 | return func(ctx context.Context, req *mcp.CallToolRequest, args ListMyRepositoriesParams) (*mcp.CallToolResult, any, error) {
240 | p := args
241 |
242 | // Build options for SDK call
243 | opt := forgejo.ListReposOptions{}
244 | // Note: ListReposOptions is quite limited in the SDK
245 | // Many filtering options are not available
246 | if p.Page > 0 {
247 | opt.Page = p.Page
248 | }
249 | if p.Limit > 0 {
250 | opt.PageSize = p.Limit
251 | }
252 |
253 | // Call SDK
254 | repos, _, err := impl.Client.ListMyRepos(opt)
255 | if err != nil {
256 | return nil, nil, fmt.Errorf("failed to list my repositories: %w", err)
257 | }
258 |
259 | // Convert to our types and format
260 | var content string
261 | if len(repos) == 0 {
262 | content = "No repositories found for the authenticated user."
263 | } else {
264 | // Convert repos to our type
265 | repoList := make(types.RepositoryList, len(repos))
266 | for i, repo := range repos {
267 | repoList[i] = &types.Repository{Repository: repo}
268 | }
269 |
270 | content = fmt.Sprintf("Found %d repositories\n\n%s",
271 | len(repos), repoList.ToMarkdown())
272 | }
273 |
274 | return &mcp.CallToolResult{
275 | Content: []mcp.Content{
276 | &mcp.TextContent{
277 | Text: content,
278 | },
279 | },
280 | }, nil, nil
281 | }
282 | }
283 |
284 | // ListOrgRepositoriesParams defines the parameters for the list_org_repositories tool.
285 | // It specifies the organization and allows for filtering and sorting.
286 | type ListOrgRepositoriesParams struct {
287 | // Org is the name of the organization.
288 | Org string `json:"org"`
289 | // Type filters repositories by their type (e.g., forks, sources).
290 | Type string `json:"type,omitempty"`
291 | // Sort specifies the sort order for the results.
292 | Sort string `json:"sort,omitempty"`
293 | // Direction specifies the sort direction (asc or desc).
294 | Direction string `json:"direction,omitempty"`
295 | // Page is the page number for pagination.
296 | Page int `json:"page,omitempty"`
297 | // Limit is the number of repositories to return per page.
298 | Limit int `json:"limit,omitempty"`
299 | }
300 |
301 | // ListOrgRepositoriesImpl implements the read-only MCP tool for listing an
302 | // organization's repositories. This is a safe, idempotent operation that uses
303 | // the Forgejo SDK.
304 | type ListOrgRepositoriesImpl struct {
305 | Client *tools.Client
306 | }
307 |
308 | // Definition describes the `list_org_repositories` tool. It requires an `org` name
309 | // and supports optional parameters for filtering and sorting. It is marked as a
310 | // safe, read-only operation.
311 | func (ListOrgRepositoriesImpl) Definition() *mcp.Tool {
312 | return &mcp.Tool{
313 | Name: "list_org_repositories",
314 | Title: "List Organization Repositories",
315 | Description: "List repositories owned by a specific organization. Returns repository information including name, description, and metadata.",
316 | Annotations: &mcp.ToolAnnotations{
317 | ReadOnlyHint: true,
318 | IdempotentHint: true,
319 | },
320 | InputSchema: &jsonschema.Schema{
321 | Type: "object",
322 | Properties: map[string]*jsonschema.Schema{
323 | "org": {
324 | Type: "string",
325 | Description: "Organization name",
326 | },
327 | "type": {
328 | Type: "string",
329 | Description: "Repository type filter: 'all', 'public', 'private', 'forks', 'sources', 'member' (optional, defaults to 'all')",
330 | Enum: []any{"all", "public", "private", "forks", "sources", "member"},
331 | },
332 | "sort": {
333 | Type: "string",
334 | Description: "Sort order: 'created', 'updated', 'pushed', 'full_name' (optional, defaults to 'created')",
335 | Enum: []any{"created", "updated", "pushed", "full_name"},
336 | },
337 | "direction": {
338 | Type: "string",
339 | Description: "Sort direction: 'asc' or 'desc' (optional, defaults to 'desc')",
340 | Enum: []any{"asc", "desc"},
341 | },
342 | "page": {
343 | Type: "integer",
344 | Description: "Page number for pagination (optional, defaults to 1)",
345 | Minimum: tools.Float64Ptr(1),
346 | },
347 | "limit": {
348 | Type: "integer",
349 | Description: "Number of repositories per page (optional, defaults to 20, max 50)",
350 | Minimum: tools.Float64Ptr(1),
351 | Maximum: tools.Float64Ptr(50),
352 | },
353 | },
354 | Required: []string{"org"},
355 | },
356 | }
357 | }
358 |
359 | // Handler implements the logic for listing organization repositories. It calls the
360 | // Forgejo SDK's `ListOrgRepos` function and formats the results into a markdown list.
361 | func (impl ListOrgRepositoriesImpl) Handler() mcp.ToolHandlerFor[ListOrgRepositoriesParams, any] {
362 | return func(ctx context.Context, req *mcp.CallToolRequest, args ListOrgRepositoriesParams) (*mcp.CallToolResult, any, error) {
363 | p := args
364 |
365 | // Build options for SDK call
366 | opt := forgejo.ListOrgReposOptions{}
367 | // Note: ListOrgReposOptions is quite limited in the SDK
368 | // Type filtering is not available
369 | if p.Page > 0 {
370 | opt.Page = p.Page
371 | }
372 | if p.Limit > 0 {
373 | opt.PageSize = p.Limit
374 | }
375 |
376 | // Call SDK
377 | repos, _, err := impl.Client.ListOrgRepos(p.Org, opt)
378 | if err != nil {
379 | return nil, nil, fmt.Errorf("failed to list organization repositories: %w", err)
380 | }
381 |
382 | // Convert to our types and format
383 | var content string
384 | if len(repos) == 0 {
385 | content = fmt.Sprintf("No repositories found for organization '%s'.", p.Org)
386 | } else {
387 | // Convert repos to our type
388 | repoList := make(types.RepositoryList, len(repos))
389 | for i, repo := range repos {
390 | repoList[i] = &types.Repository{Repository: repo}
391 | }
392 |
393 | content = fmt.Sprintf("Found %d repositories for organization '%s'\n\n%s",
394 | len(repos), p.Org, repoList.ToMarkdown())
395 | }
396 |
397 | return &mcp.CallToolResult{
398 | Content: []mcp.Content{
399 | &mcp.TextContent{
400 | Text: content,
401 | },
402 | },
403 | }, nil, nil
404 | }
405 | }
406 |
407 | // GetRepositoryParams defines the parameters for the get_repository tool.
408 | // It specifies the owner and repository name to retrieve.
409 | type GetRepositoryParams struct {
410 | // Owner is the username or organization name that owns the repository.
411 | Owner string `json:"owner"`
412 | // Repo is the name of the repository.
413 | Repo string `json:"repo"`
414 | }
415 |
416 | // GetRepositoryImpl implements the read-only MCP tool for fetching detailed
417 | // information about a single repository. This is a safe, idempotent operation
418 | // that uses the Forgejo SDK.
419 | type GetRepositoryImpl struct {
420 | Client *tools.Client
421 | }
422 |
423 | // Definition describes the `get_repository` tool. It requires `owner` and `repo`
424 | // as parameters and is marked as a safe, read-only operation.
425 | func (GetRepositoryImpl) Definition() *mcp.Tool {
426 | return &mcp.Tool{
427 | Name: "get_repository",
428 | Title: "Get Repository Information",
429 | Description: "Get detailed information about a specific repository, including description, stats, permissions, and metadata.",
430 | Annotations: &mcp.ToolAnnotations{
431 | ReadOnlyHint: true,
432 | IdempotentHint: true,
433 | },
434 | InputSchema: &jsonschema.Schema{
435 | Type: "object",
436 | Properties: map[string]*jsonschema.Schema{
437 | "owner": {
438 | Type: "string",
439 | Description: "Repository owner (username or organization name)",
440 | },
441 | "repo": {
442 | Type: "string",
443 | Description: "Repository name",
444 | },
445 | },
446 | Required: []string{"owner", "repo"},
447 | },
448 | }
449 | }
450 |
451 | // Handler implements the logic for fetching repository details. It calls the
452 | // Forgejo SDK's `GetRepo` function and formats the full repository object into
453 | // a detailed markdown view.
454 | func (impl GetRepositoryImpl) Handler() mcp.ToolHandlerFor[GetRepositoryParams, any] {
455 | return func(ctx context.Context, req *mcp.CallToolRequest, args GetRepositoryParams) (*mcp.CallToolResult, any, error) {
456 | p := args
457 |
458 | // Call SDK
459 | repo, _, err := impl.Client.GetRepo(p.Owner, p.Repo)
460 | if err != nil {
461 | return nil, nil, fmt.Errorf("failed to get repository: %w", err)
462 | }
463 |
464 | // Convert to our type and format
465 | repoWrapper := &types.Repository{Repository: repo}
466 |
467 | return &mcp.CallToolResult{
468 | Content: []mcp.Content{
469 | &mcp.TextContent{
470 | Text: repoWrapper.ToMarkdown(),
471 | },
472 | },
473 | }, nil, nil
474 | }
475 | }
476 |
```