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