This is page 2 of 2. Use http://codebase.md/raohwork/forgejo-mcp?lines=false&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/label/crud.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package label
import (
"context"
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListRepoLabelsParams defines the parameters for the list_repo_labels tool.
// It specifies the owner and repository name to list labels from.
type ListRepoLabelsParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
}
// ListRepoLabelsImpl implements the read-only MCP tool for listing repository labels.
// This operation is safe, idempotent, and does not modify any data. It fetches
// all available labels for a specified repository using the Forgejo SDK.
type ListRepoLabelsImpl struct {
Client *tools.Client
}
// Definition describes the `list_repo_labels` tool. It requires `owner` and `repo`
// as parameters and is marked as a safe, read-only operation.
func (ListRepoLabelsImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_repo_labels",
Title: "List Repository Labels",
Description: "List all labels available in a repository. Returns label information including name, description, color, and ID.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
},
Required: []string{"owner", "repo"},
},
}
}
// Handler implements the logic for listing labels. It calls the Forgejo SDK's
// `ListRepoLabels` function and formats the resulting slice of labels into
// a markdown list. Errors will occur if the repository is not found or
// authentication fails.
func (impl ListRepoLabelsImpl) Handler() mcp.ToolHandlerFor[ListRepoLabelsParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListRepoLabelsParams) (*mcp.CallToolResult, any, error) {
p := args
// Call SDK
labels, _, err := impl.Client.ListRepoLabels(p.Owner, p.Repo, forgejo.ListLabelsOptions{})
if err != nil {
return nil, nil, fmt.Errorf("failed to list labels: %w", err)
}
// Convert to our types and format
var content string
if len(labels) == 0 {
content = "No labels found in this repository."
} else {
// Convert labels to our type
labelList := make(types.LabelList, len(labels))
for i, label := range labels {
labelList[i] = &types.Label{Label: label}
}
content = fmt.Sprintf("Found %d labels\n\n%s",
len(labels), labelList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// CreateLabelParams defines the parameters for the create_label tool.
// It includes the label's name, color, and optional description.
type CreateLabelParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Name is the name of the new label.
Name string `json:"name"`
// Color is the hex color code for the label (without the '#').
Color string `json:"color"`
// Description is the optional markdown description of the label.
Description string `json:"description,omitempty"`
}
// CreateLabelImpl implements the MCP tool for creating a new repository label.
// This is a non-idempotent operation that creates a new label using the Forgejo SDK.
type CreateLabelImpl struct {
Client *tools.Client
}
// Definition describes the `create_label` tool. It requires `owner`, `repo`,
// a `name`, and a `color`. It is not idempotent, as multiple calls with the
// same name will fail once the first label is created.
func (CreateLabelImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "create_label",
Title: "Create Label",
Description: "Create a new label in a repository. Specify the label name, description, and color.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: false,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"name": {
Type: "string",
Description: "Label name",
},
"color": {
Type: "string",
Description: "Label color (hex color code without #, e.g., 'ff0000' for red)",
},
"description": {
Type: "string",
Description: "Optional label description",
},
},
Required: []string{"owner", "repo", "name", "color"},
},
}
}
// Handler implements the logic for creating a label. It calls the Forgejo SDK's
// `CreateLabel` function and returns the details of the newly created label.
func (impl CreateLabelImpl) Handler() mcp.ToolHandlerFor[CreateLabelParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args CreateLabelParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.CreateLabelOption{
Name: p.Name,
Color: p.Color,
Description: p.Description,
}
// Call SDK
label, _, err := impl.Client.CreateLabel(p.Owner, p.Repo, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to create label: %w", err)
}
// Convert to our type and format
labelWrapper := &types.Label{Label: label}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: labelWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// EditLabelParams defines the parameters for the edit_label tool.
// It specifies the label to edit by ID and the fields to update.
type EditLabelParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// ID is the unique identifier of the label to edit.
ID int `json:"id"`
// Name is the new name for the label.
Name string `json:"name,omitempty"`
// Color is the new hex color code for the label (without the '#').
Color string `json:"color,omitempty"`
// Description is the new optional markdown description for the label.
Description string `json:"description,omitempty"`
}
// EditLabelImpl implements the MCP tool for editing an existing repository label.
// This is an idempotent operation that modifies a label's metadata using the
// Forgejo SDK.
type EditLabelImpl struct {
Client *tools.Client
}
// Definition describes the `edit_label` tool. It requires `owner`, `repo`, and the
// label `id`. It is marked as idempotent.
func (EditLabelImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "edit_label",
Title: "Edit Label",
Description: "Edit an existing label's name, description, or color.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"id": {
Type: "integer",
Description: "Label ID",
},
"name": {
Type: "string",
Description: "New label name (optional)",
},
"color": {
Type: "string",
Description: "New label color (hex color code without #, e.g., 'ff0000' for red) (optional)",
},
"description": {
Type: "string",
Description: "New label description (optional)",
},
},
Required: []string{"owner", "repo", "id"},
},
}
}
// Handler implements the logic for editing a label. It calls the Forgejo SDK's
// `EditLabel` function. It will return an error if the label ID is not found.
func (impl EditLabelImpl) Handler() mcp.ToolHandlerFor[EditLabelParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args EditLabelParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.EditLabelOption{}
if p.Name != "" {
opt.Name = &p.Name
}
if p.Color != "" {
opt.Color = &p.Color
}
if p.Description != "" {
opt.Description = &p.Description
}
// Call SDK
label, _, err := impl.Client.EditLabel(p.Owner, p.Repo, int64(p.ID), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to edit label: %w", err)
}
// Convert to our type and format
labelWrapper := &types.Label{Label: label}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: labelWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// DeleteLabelParams defines the parameters for the delete_label tool.
// It specifies the label to be deleted by its ID.
type DeleteLabelParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// ID is the unique identifier of the label to delete.
ID int `json:"id"`
}
// DeleteLabelImpl implements the destructive MCP tool for deleting a repository label.
// This is an idempotent but irreversible operation that removes a label from a
// repository using the Forgejo SDK.
type DeleteLabelImpl struct {
Client *tools.Client
}
// Definition describes the `delete_label` tool. It requires `owner`, `repo`, and
// the label `id`. It is marked as a destructive operation to ensure clients
// can warn the user before execution.
func (DeleteLabelImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "delete_label",
Title: "Delete Label",
Description: "Delete a label from a repository.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(true),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"id": {
Type: "integer",
Description: "Label ID to delete",
},
},
Required: []string{"owner", "repo", "id"},
},
}
}
// Handler implements the logic for deleting a label. It calls the Forgejo SDK's
// `DeleteLabel` function. On success, it returns a simple text confirmation.
// It will return an error if the label does not exist.
func (impl DeleteLabelImpl) Handler() mcp.ToolHandlerFor[DeleteLabelParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteLabelParams) (*mcp.CallToolResult, any, error) {
p := args
// Call SDK
_, err := impl.Client.DeleteLabel(p.Owner, p.Repo, int64(p.ID))
if err != nil {
return nil, nil, fmt.Errorf("failed to delete label: %w", err)
}
// Return success message
emptyResponse := types.EmptyResponse{}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: emptyResponse.ToMarkdown(),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/wiki/crud.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package wiki
import (
"context"
"encoding/base64"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// GetWikiPageParams defines the parameters for the get_wiki_page tool.
// It specifies the owner, repository, and page name to retrieve.
type GetWikiPageParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// PageName is the title of the wiki page to retrieve.
PageName string `json:"page_name"`
}
// GetWikiPageImpl implements the read-only MCP tool for fetching a single wiki page.
// This operation is safe, idempotent, and does not modify any data. Note: This
// feature is not supported by the official Forgejo SDK and requires a custom
// HTTP implementation.
type GetWikiPageImpl struct {
Client *tools.Client
}
// Definition describes the `get_wiki_page` tool. It requires `owner`, `repo`,
// and `page_name` as parameters and is marked as a safe, read-only operation.
func (GetWikiPageImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "get_wiki_page",
Title: "Get Wiki Page",
Description: "Get the content and metadata of a specific wiki page.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"page_name": {
Type: "string",
Description: "Wiki page name",
},
},
Required: []string{"owner", "repo", "page_name"},
},
}
}
// Handler implements the logic for fetching a wiki page. It performs a custom
// HTTP GET request to the `/repos/{owner}/{repo}/wiki/page/{pageName}` endpoint
// and formats the resulting page content as markdown. Errors will occur if the
// page or repository is not found.
func (impl GetWikiPageImpl) Handler() mcp.ToolHandlerFor[GetWikiPageParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args GetWikiPageParams) (*mcp.CallToolResult, any, error) {
p := args
// Call custom client method
page, err := impl.Client.MyGetWikiPage(p.Owner, p.Repo, p.PageName)
if err != nil {
return nil, nil, fmt.Errorf("failed to get wiki page: %w", err)
}
// Convert to our type and format
wikiPage := &types.WikiPage{
MyWikiPage: page,
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: wikiPage.ToMarkdown(),
},
},
}, nil, nil
}
}
// CreateWikiPageParams defines the parameters for the create_wiki_page tool.
// It includes the title and content for the new wiki page.
type CreateWikiPageParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Title is the title for the new wiki page.
Title string `json:"title"`
// Content is the markdown content of the new wiki page.
Content string `json:"content"`
// Message is an optional commit message for the creation.
Message string `json:"message,omitempty"`
}
// CreateWikiPageImpl implements the MCP tool for creating a new wiki page.
// This is a non-idempotent operation that adds a new page to the repository's
// wiki. Note: This feature is not supported by the official Forgejo SDK and
// requires a custom HTTP implementation.
type CreateWikiPageImpl struct {
Client *tools.Client
}
// Definition describes the `create_wiki_page` tool. It requires `owner`, `repo`,
// `title`, and `content`. It is not idempotent as multiple calls with the same
// parameters will result in multiple pages if the title is not unique.
func (CreateWikiPageImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "create_wiki_page",
Title: "Create Wiki Page",
Description: "Create a new wiki page with specified title and content.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: false,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"title": {
Type: "string",
Description: "Wiki page title",
},
"content": {
Type: "string",
Description: "Wiki page content (markdown supported)",
},
"message": {
Type: "string",
Description: "Optional commit message (defaults to 'Create page {title}')",
},
},
Required: []string{"owner", "repo", "title", "content"},
},
}
}
// Handler implements the logic for creating a wiki page. It performs a custom
// HTTP POST request to the `/repos/{owner}/{repo}/wiki/new` endpoint. On success,
// it returns information about the newly created page.
func (impl CreateWikiPageImpl) Handler() mcp.ToolHandlerFor[CreateWikiPageParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args CreateWikiPageParams) (*mcp.CallToolResult, any, error) {
p := args
// Prepare options for API call
options := types.MyCreateWikiPageOptions{
Title: p.Title,
ContentBase64: base64.StdEncoding.EncodeToString([]byte(p.Content)),
Message: p.Message,
}
// Call custom client method
page, err := impl.Client.MyCreateWikiPage(p.Owner, p.Repo, options)
if err != nil {
return nil, nil, fmt.Errorf("failed to create wiki page: %w", err)
}
// Convert to our type and format
wikiPage := &types.WikiPage{
MyWikiPage: page,
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: wikiPage.ToMarkdown(),
},
},
}, nil, nil
}
}
// EditWikiPageParams defines the parameters for the edit_wiki_page tool.
// It specifies the page to edit and the new content.
type EditWikiPageParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// PageName is the current title of the wiki page to edit.
PageName string `json:"page_name"`
// Title is the optional new title for the wiki page.
Title string `json:"title,omitempty"`
// Content is the new markdown content for the wiki page.
Content string `json:"content"`
// Message is an optional commit message for the update.
Message string `json:"message,omitempty"`
}
// EditWikiPageImpl implements the MCP tool for editing an existing wiki page.
// This is an idempotent operation. Note: This feature is not supported by the
// official Forgejo SDK and requires a custom HTTP implementation.
type EditWikiPageImpl struct {
Client *tools.Client
}
// Definition describes the `edit_wiki_page` tool. It requires `owner`, `repo`,
// `page_name`, and new `content`. It is marked as idempotent.
func (EditWikiPageImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "edit_wiki_page",
Title: "Edit Wiki Page",
Description: "Edit an existing wiki page's title and content.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"page_name": {
Type: "string",
Description: "Wiki page name to edit",
},
"title": {
Type: "string",
Description: "New wiki page title (optional, defaults to current title)",
},
"content": {
Type: "string",
Description: "New wiki page content (markdown supported)",
},
"message": {
Type: "string",
Description: "Optional commit message (defaults to 'Update page {page_name}')",
},
},
Required: []string{"owner", "repo", "page_name", "content"},
},
}
}
// Handler implements the logic for editing a wiki page. It performs a custom
// HTTP PATCH request to the `/repos/{owner}/{repo}/wiki/page/{pageName}` endpoint.
// It returns an error if the page is not found.
func (impl EditWikiPageImpl) Handler() mcp.ToolHandlerFor[EditWikiPageParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args EditWikiPageParams) (*mcp.CallToolResult, any, error) {
p := args
// Prepare options for API call
title := p.Title
if title == "" {
title = p.PageName // Use current name if no new title
}
options := types.MyCreateWikiPageOptions{
Title: title,
ContentBase64: base64.StdEncoding.EncodeToString([]byte(p.Content)),
Message: p.Message,
}
// Call custom client method
page, err := impl.Client.MyEditWikiPage(p.Owner, p.Repo, p.PageName, options)
if err != nil {
return nil, nil, fmt.Errorf("failed to edit wiki page: %w", err)
}
// Convert to our type and format
wikiPage := &types.WikiPage{
MyWikiPage: page,
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: wikiPage.ToMarkdown(),
},
},
}, nil, nil
}
}
// DeleteWikiPageParams defines the parameters for the delete_wiki_page tool.
// It specifies the page to be deleted.
type DeleteWikiPageParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// PageName is the title of the wiki page to delete.
PageName string `json:"page_name"`
}
// DeleteWikiPageImpl implements the destructive MCP tool for deleting a wiki page.
// This is an idempotent but irreversible operation. Note: This feature is not
// supported by the official Forgejo SDK and requires a custom HTTP implementation.
type DeleteWikiPageImpl struct {
Client *tools.Client
}
// Definition describes the `delete_wiki_page` tool. It requires `owner`, `repo`,
// and `page_name`. It is marked as a destructive operation to ensure clients
// can warn the user before execution.
func (DeleteWikiPageImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "delete_wiki_page",
Title: "Delete Wiki Page",
Description: "Delete a wiki page from the repository.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(true),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"page_name": {
Type: "string",
Description: "Wiki page name to delete",
},
},
Required: []string{"owner", "repo", "page_name"},
},
}
}
// Handler implements the logic for deleting a wiki page. It performs a custom
// HTTP DELETE request to the `/repos/{owner}/{repo}/wiki/page/{pageName}` endpoint.
// On success, it returns a simple text confirmation.
func (impl DeleteWikiPageImpl) Handler() mcp.ToolHandlerFor[DeleteWikiPageParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteWikiPageParams) (*mcp.CallToolResult, any, error) {
p := args
// Call custom client method
err := impl.Client.MyDeleteWikiPage(p.Owner, p.Repo, p.PageName)
if err != nil {
return nil, nil, fmt.Errorf("failed to delete wiki page: %w", err)
}
// Return success message
emptyResponse := types.EmptyResponse{}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: emptyResponse.ToMarkdown(),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/client_test.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package tools
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
const forgejo_version_to_test = "11.0.1+gitea-1.22.0"
// sendSimpleRequest Specification:
//
// Responsibility: Handle all pure JSON API requests, including Issue Dependencies,
// Wiki Pages, Forgejo Actions, and Issue Attachments non-upload operations
//
// Business Logic:
// 1. Create HTTP request (using provided method and endpoint)
// 2. If paramObj is not nil, serialize it as JSON for request body
// 3. Use Forgejo SDK's SignRequest method to add authentication headers
// 4. Send request and receive response
// 5. Deserialize JSON response to respObj
// 6. Handle HTTP error status codes
//
// Parameters:
// - method: HTTP method (GET, POST, PATCH, DELETE)
// - endpoint: API endpoint path (relative to base URL)
// - paramObj: request parameter object (JSON serialized), can be nil for GET/DELETE
// - respObj: response data receiver object (JSON deserialized)
//
// Returns: error if request fails or response cannot be parsed
//
// Design Philosophy:
// - Focus on our project needs, not pursuing genericity
// - Rely on Forgejo SDK for authentication, we only handle request/response serialization
// - Simplified error handling, mainly focus on network errors and JSON parsing errors
//
// Implementation Notes:
// - Need to combine c.base and endpoint to form complete URL
// - Content-Type should be set to application/json (when request body exists)
// - Accept should be set to application/json
// - HTTP 4xx/5xx status codes should return errors
func TestClient_sendSimpleRequest(t *testing.T) {
// GET request test - no request body
t.Run("GET_success", func(t *testing.T) {
// Mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("Expected GET method, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/owner/repo/issues/1/dependencies" {
t.Errorf("Expected specific path, got %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 1,
"title": "Test Issue",
})
}))
defer server.Close()
client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
var result map[string]interface{}
err = client.sendSimpleRequest("GET", "/api/v1/repos/owner/repo/issues/1/dependencies", nil, &result)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result["id"] != float64(1) {
t.Errorf("Expected id=1, got %v", result["id"])
}
if result["title"] != "Test Issue" {
t.Errorf("Expected title='Test Issue', got %v", result["title"])
}
})
// POST request test - with request body
t.Run("POST_success", func(t *testing.T) {
// Mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("Expected POST method, got %s", r.Method)
}
if r.Header.Get("Content-Type") != "application/json" {
t.Errorf("Expected Content-Type: application/json, got %s", r.Header.Get("Content-Type"))
}
var reqBody map[string]interface{}
json.NewDecoder(r.Body).Decode(&reqBody)
if reqBody["index"] != float64(2) {
t.Errorf("Expected index=2, got %v", reqBody["index"])
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "dependency added",
})
}))
defer server.Close()
client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
requestData := map[string]interface{}{"index": 2}
var result map[string]interface{}
err = client.sendSimpleRequest("POST", "/api/v1/repos/owner/repo/issues/1/dependencies", requestData, &result)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result["message"] != "dependency added" {
t.Errorf("Expected message='dependency added', got %v", result["message"])
}
})
// HTTP error handling test
t.Run("HTTP_error", func(t *testing.T) {
// Mock server returning 404
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "Not found",
})
}))
defer server.Close()
client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
var result map[string]interface{}
err = client.sendSimpleRequest("GET", "/api/v1/repos/owner/repo/nonexistent", nil, &result)
if err == nil {
t.Error("Expected error for 404 response, got nil")
}
})
// JSON parsing error test
t.Run("JSON_parse_error", func(t *testing.T) {
// Mock server returning invalid JSON
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("invalid json"))
}))
defer server.Close()
client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
var result map[string]interface{}
err = client.sendSimpleRequest("GET", "/api/v1/repos/owner/repo/issues", nil, &result)
if err == nil {
t.Error("Expected JSON parsing error, got nil")
}
})
}
// sendUploadRequest Specification:
//
// Responsibility: Handle file upload requests, currently mainly for Issue Attachment creation
//
// Business Logic:
// 1. Create multipart/form-data format HTTP POST request
// 2. Add file to multipart writer (using filename)
// 3. Add additional form fields from extraFields
// 4. Use Forgejo SDK's SignRequest method to add authentication headers
// 5. Send request and receive response
// 6. Deserialize JSON response to respObj
//
// Parameters:
// - endpoint: API endpoint path (fixed to use POST method)
// - filename: upload file name
// - file: file content (io.Reader)
// - extraFields: additional form fields (e.g. name, updated_at)
// - respObj: response data receiver object
//
// Returns: error if upload fails or response cannot be parsed
//
// Design Philosophy:
// - Focus on Issue Attachment upload requirements
// - Use standard multipart/form-data format
// - Rely on Forgejo SDK for authentication
//
// Implementation Notes:
// - Content-Type will be automatically set by multipart.Writer
// - File field name should be "attachment" (according to Forgejo API)
// - Accept should be set to application/json
// - Need to correctly handle multipart boundary
func TestClient_sendUploadRequest(t *testing.T) {
// Successful upload test
t.Run("upload_success", func(t *testing.T) {
// Mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("Expected POST method, got %s", r.Method)
}
if !strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") {
t.Errorf("Expected multipart/form-data Content-Type, got %s", r.Header.Get("Content-Type"))
}
// Parse multipart form
err := r.ParseMultipartForm(32 << 20) // 32MB
if err != nil {
t.Errorf("Failed to parse multipart form: %v", err)
}
// Check extra fields
if r.FormValue("name") != "test.txt" {
t.Errorf("Expected name='test.txt', got %s", r.FormValue("name"))
}
// Check file
file, header, err := r.FormFile("attachment")
if err != nil {
t.Errorf("Failed to get file: %v", err)
}
defer file.Close()
if header.Filename != "test.txt" {
t.Errorf("Expected filename='test.txt', got %s", header.Filename)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"id": "123",
"name": "test.txt",
"download_url": "http://example.com/download/123",
})
}))
defer server.Close()
client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
file := strings.NewReader("test file content")
extraFields := map[string]string{"name": "test.txt"}
var result map[string]interface{}
err = client.sendUploadRequest("/api/v1/repos/owner/repo/issues/1/assets", "test.txt", file, extraFields, &result)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if result["id"] != "123" {
t.Errorf("Expected id='123', got %v", result["id"])
}
if result["name"] != "test.txt" {
t.Errorf("Expected name='test.txt', got %v", result["name"])
}
})
// Empty file upload test
t.Run("empty_file", func(t *testing.T) {
// Mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("Expected POST method, got %s", r.Method)
}
err := r.ParseMultipartForm(32 << 20)
if err != nil {
t.Errorf("Failed to parse multipart form: %v", err)
}
file, header, err := r.FormFile("attachment")
if err != nil {
t.Errorf("Failed to get file: %v", err)
}
defer file.Close()
if header.Filename != "empty.txt" {
t.Errorf("Expected filename='empty.txt', got %s", header.Filename)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"id": "124",
"name": "empty.txt",
})
}))
defer server.Close()
client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
file := strings.NewReader("")
var result map[string]interface{}
err = client.sendUploadRequest("/api/v1/repos/owner/repo/issues/1/assets", "empty.txt", file, nil, &result)
if err != nil {
t.Errorf("Expected no error for empty file, got %v", err)
}
if result["id"] != "124" {
t.Errorf("Expected id='124', got %v", result["id"])
}
})
// Upload error test
t.Run("upload_error", func(t *testing.T) {
// Mock server returning 500 error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "Internal server error",
})
}))
defer server.Close()
client, err := NewClient(server.URL, "test-token", forgejo_version_to_test, server.Client())
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
file := strings.NewReader("test content")
var result map[string]interface{}
err = client.sendUploadRequest("/api/v1/repos/owner/repo/issues/1/assets", "test.txt", file, nil, &result)
if err == nil {
t.Error("Expected error for 500 response, got nil")
}
})
}
// DO NOT TEST AGAINST PRODUCTION FORGEJO SERVERS
// DO NOT TEST AGAINST REPO THAT HAS MORE THAN 50 WORKFLOW RUNS
func TestCustomClient_Integral(t *testing.T) {
server := os.Getenv("FORGEJO_TEST_SERVER")
token := os.Getenv("FORGEJO_TEST_TOKEN")
repo := os.Getenv("FORGEJO_TEST_REPO")
if server == "" || token == "" || repo == "" {
t.Skip("Skipping test, FORGEJO_TEST_SERVER, FORGEJO_TEST_TOKEN, and FORGEJO_TEST_REPO must be set")
}
arr := strings.Split(repo, "/")
if len(arr) != 2 {
t.Fatalf("Invalid repo format, expected 'owner/repo', got '%s'", repo)
}
t.Logf("Using server: %s, repo: %s", server, repo)
cl, err := NewClient(server, token, "", nil)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
resp, err := cl.MyListActionTasks(arr[0], arr[1])
if err != nil {
t.Fatalf("Failed to list action tasks: %v", err)
}
if resp == nil {
t.Fatal("Expected non-nil response, got nil")
}
if resp.TotalCount < 0 {
t.Errorf("Expected TotalCount >= 0, got %d", resp.TotalCount)
}
if len(resp.WorkflowRuns) != int(resp.TotalCount) {
t.Errorf("Expected WorkflowRuns length %d, got %d", resp.TotalCount, len(resp.WorkflowRuns))
}
if len(resp.WorkflowRuns) > 0 {
t.Logf("First WorkflowRun ID: %d, Name: %s, Status: %s", resp.WorkflowRuns[0].ID, resp.WorkflowRuns[0].Name, resp.WorkflowRuns[0].Status)
}
}
```
--------------------------------------------------------------------------------
/tools/issue/comment.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package issue
import (
"context"
"fmt"
"time"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListIssueCommentsParams defines the parameters for the list_issue_comments tool.
// It specifies the issue and includes optional filters for pagination and time range.
type ListIssueCommentsParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
// Since filters for comments updated after the given time.
Since time.Time `json:"since,omitempty"`
// Before filters for comments updated before the given time.
Before time.Time `json:"before,omitempty"`
// Page is the page number for pagination.
Page int `json:"page,omitempty"`
// Limit is the number of comments to return per page.
Limit int `json:"limit,omitempty"`
}
// ListIssueCommentsImpl implements the read-only MCP tool for listing issue comments.
// This is a safe, idempotent operation that uses the Forgejo SDK to fetch a list
// of comments for a specific issue.
type ListIssueCommentsImpl struct {
Client *tools.Client
}
// Definition describes the `list_issue_comments` tool. It requires `owner`, `repo`,
// and the issue `index`. It supports time-based filtering and pagination and is
// marked as a safe, read-only operation.
func (ListIssueCommentsImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_issue_comments",
Title: "List Issue Comments",
Description: "List all comments on a specific issue, including comment body, author, and timestamps.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"since": {
Type: "string",
Description: "Only show comments updated after this time (ISO 8601 format) (optional)",
Format: "date-time",
},
"before": {
Type: "string",
Description: "Only show comments updated before this time (ISO 8601 format) (optional)",
Format: "date-time",
},
"page": {
Type: "integer",
Description: "Page number for pagination (optional, defaults to 1)",
Minimum: tools.Float64Ptr(1),
},
"limit": {
Type: "integer",
Description: "Number of comments per page (optional, defaults to 20, max 50)",
Minimum: tools.Float64Ptr(1),
Maximum: tools.Float64Ptr(50),
},
},
Required: []string{"owner", "repo", "index"},
},
}
}
// Handler implements the logic for listing issue comments. It calls the Forgejo SDK's
// `ListIssueComments` function and formats the results into a markdown list.
func (impl ListIssueCommentsImpl) Handler() mcp.ToolHandlerFor[ListIssueCommentsParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListIssueCommentsParams) (*mcp.CallToolResult, any, error) {
p := args
opt := forgejo.ListIssueCommentOptions{}
if !p.Since.IsZero() {
opt.Since = p.Since
}
if !p.Before.IsZero() {
opt.Before = p.Before
}
if p.Page > 0 {
opt.Page = p.Page
}
if p.Limit > 0 {
opt.PageSize = p.Limit
}
comments, _, err := impl.Client.ListIssueComments(p.Owner, p.Repo, int64(p.Index), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to list comments: %w", err)
}
var content string
if len(comments) == 0 {
content = "No comments found for this issue."
} else {
var commentsMarkdown string
for _, comment := range comments {
commentWrapper := &types.Comment{Comment: comment}
commentsMarkdown += commentWrapper.ToMarkdown() + "\n\n---\n\n"
}
content = fmt.Sprintf("Found %d comments\n\n%s", len(comments), commentsMarkdown)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// CreateIssueCommentParams defines the parameters for the create_issue_comment tool.
// It specifies the issue to comment on and the content of the comment.
type CreateIssueCommentParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
// Body is the markdown content of the comment.
Body string `json:"body"`
}
// CreateIssueCommentImpl implements the MCP tool for creating a new comment on an issue.
// This is a non-idempotent operation that posts a new comment using the Forgejo SDK.
type CreateIssueCommentImpl struct {
Client *tools.Client
}
// Definition describes the `create_issue_comment` tool. It requires the issue `index`
// and the comment `body`. It is not idempotent, as multiple calls will create
// multiple identical comments.
func (CreateIssueCommentImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "create_issue_comment",
Title: "Add Issue Comment",
Description: "Add a comment to an existing issue.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: false,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"body": {
Type: "string",
Description: "Comment body content (markdown supported)",
},
},
Required: []string{"owner", "repo", "index", "body"},
},
}
}
// Handler implements the logic for creating an issue comment. It calls the Forgejo
// SDK's `CreateIssueComment` function and returns the details of the new comment.
func (impl CreateIssueCommentImpl) Handler() mcp.ToolHandlerFor[CreateIssueCommentParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args CreateIssueCommentParams) (*mcp.CallToolResult, any, error) {
p := args
opt := forgejo.CreateIssueCommentOption{
Body: p.Body,
}
comment, _, err := impl.Client.CreateIssueComment(p.Owner, p.Repo, int64(p.Index), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to create comment: %w", err)
}
// reply the id of the comment
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("Comment#%d has been created successfully.", comment.ID),
},
},
}, nil, nil
}
}
// EditIssueCommentParams defines the parameters for the edit_issue_comment tool.
// It specifies the comment to edit by its ID and the new content.
type EditIssueCommentParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// CommentID is the unique identifier of the comment to edit.
CommentID int `json:"comment_id"`
// Body is the new markdown content for the comment.
Body string `json:"body"`
}
// EditIssueCommentImpl implements the MCP tool for editing an existing issue comment.
// This is an idempotent operation that modifies a comment's content using the
// Forgejo SDK.
type EditIssueCommentImpl struct {
Client *tools.Client
}
// Definition describes the `edit_issue_comment` tool. It requires the `comment_id`
// and the new `body`. It is marked as idempotent.
func (EditIssueCommentImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "edit_issue_comment",
Title: "Edit Issue Comment",
Description: "Edit an existing comment on an issue. You can modify the comment body content.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"comment_id": {
Type: "integer",
Description: "Comment ID to edit",
},
"body": {
Type: "string",
Description: "New comment body content (markdown supported)",
},
},
Required: []string{"owner", "repo", "comment_id", "body"},
},
}
}
// Handler implements the logic for editing an issue comment. It calls the Forgejo
// SDK's `EditIssueComment` function. It will return an error if the comment ID
// is not found.
func (impl EditIssueCommentImpl) Handler() mcp.ToolHandlerFor[EditIssueCommentParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args EditIssueCommentParams) (*mcp.CallToolResult, any, error) {
p := args
opt := forgejo.EditIssueCommentOption{
Body: p.Body,
}
comment, _, err := impl.Client.EditIssueComment(p.Owner, p.Repo, int64(p.CommentID), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to edit comment: %w", err)
}
commentWrapper := &types.Comment{Comment: comment}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: commentWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// DeleteIssueCommentParams defines the parameters for the delete_issue_comment tool.
// It specifies the comment to be deleted by its ID.
type DeleteIssueCommentParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// CommentID is the unique identifier of the comment to delete.
CommentID int `json:"comment_id"`
}
// DeleteIssueCommentImpl implements the destructive MCP tool for deleting an issue comment.
// This is an idempotent but irreversible operation that removes a comment using the
// Forgejo SDK.
type DeleteIssueCommentImpl struct {
Client *tools.Client
}
// Definition describes the `delete_issue_comment` tool. It requires the `comment_id`
// to be deleted. It is marked as a destructive operation to ensure clients can
// warn the user before execution.
func (DeleteIssueCommentImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "delete_issue_comment",
Title: "Delete Issue Comment",
Description: "Delete a specific comment from an issue. This action cannot be undone.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(true),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"comment_id": {
Type: "integer",
Description: "Comment ID to delete",
},
},
Required: []string{"owner", "repo", "comment_id"},
},
}
}
// Handler implements the logic for deleting an issue comment. It calls the Forgejo
// SDK's `DeleteIssueComment` function. On success, it returns a simple text
// confirmation. It will return an error if the comment does not exist.
func (impl DeleteIssueCommentImpl) Handler() mcp.ToolHandlerFor[DeleteIssueCommentParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteIssueCommentParams) (*mcp.CallToolResult, any, error) {
p := args
_, err := impl.Client.DeleteIssueComment(p.Owner, p.Repo, int64(p.CommentID))
if err != nil {
return nil, nil, fmt.Errorf("failed to delete comment: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("Comment %d successfully deleted.", p.CommentID),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/milestone/crud.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package milestone
import (
"context"
"fmt"
"time"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListRepoMilestonesParams defines the parameters for the list_repo_milestones tool.
// It specifies the repository and an optional state filter.
type ListRepoMilestonesParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// State filters milestones by their state (e.g., 'open', 'closed').
State string `json:"state,omitempty"`
}
// ListRepoMilestonesImpl implements the read-only MCP tool for listing repository milestones.
// This is a safe, idempotent operation that uses the Forgejo SDK to fetch a list
// of milestones, optionally filtered by their state.
type ListRepoMilestonesImpl struct {
Client *tools.Client
}
// Definition describes the `list_repo_milestones` tool. It requires `owner` and `repo`,
// supports an optional `state` filter, and is marked as a safe, read-only operation.
func (ListRepoMilestonesImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_repo_milestones",
Title: "List Repository Milestones",
Description: "List all milestones in a repository. Returns milestone information including title, description, due date, and status.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"state": {
Type: "string",
Description: "Milestone state filter: 'open', 'closed', or 'all' (optional, defaults to 'open')",
Enum: []any{"open", "closed", "all"},
},
},
Required: []string{"owner", "repo"},
},
}
}
// Handler implements the logic for listing milestones. It calls the Forgejo SDK's
// `ListRepoMilestones` function and formats the results into a markdown list.
func (impl ListRepoMilestonesImpl) Handler() mcp.ToolHandlerFor[ListRepoMilestonesParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListRepoMilestonesParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.ListMilestoneOption{}
if p.State != "" {
opt.State = forgejo.StateType(p.State)
}
// Call SDK
milestones, _, err := impl.Client.ListRepoMilestones(p.Owner, p.Repo, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to list milestones: %w", err)
}
// Convert to our types and format
var content string
if len(milestones) == 0 {
content = "No milestones found in this repository."
} else {
// Convert milestones to our type
milestoneList := make(types.MilestoneList, len(milestones))
for i, milestone := range milestones {
milestoneList[i] = &types.Milestone{Milestone: milestone}
}
content = fmt.Sprintf("Found %d milestones\n\n%s",
len(milestones), milestoneList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// CreateMilestoneParams defines the parameters for the create_milestone tool.
// It includes the title and optional description and due date.
type CreateMilestoneParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Title is the title of the new milestone.
Title string `json:"title"`
// Description is the markdown description of the milestone.
Description string `json:"description,omitempty"`
// DueDate is the optional due date for the milestone.
DueDate time.Time `json:"due_date,omitempty"`
}
// CreateMilestoneImpl implements the MCP tool for creating a new milestone.
// This is a non-idempotent operation that creates a new milestone object
// using the Forgejo SDK.
type CreateMilestoneImpl struct {
Client *tools.Client
}
// Definition describes the `create_milestone` tool. It requires `owner`, `repo`,
// and a `title`. It is not idempotent, as multiple calls with the same title
// will create multiple milestones.
func (CreateMilestoneImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "create_milestone",
Title: "Create Milestone",
Description: "Create a new milestone in a repository. Specify the title, description, and optional due date.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: false,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"title": {
Type: "string",
Description: "Milestone title",
},
"description": {
Type: "string",
Description: "Milestone description (optional)",
},
"due_date": {
Type: "string",
Description: "Milestone due date in ISO 8601 format (e.g., '2024-12-31T23:59:59Z') (optional)",
Format: "date-time",
},
},
Required: []string{"owner", "repo", "title"},
},
}
}
// Handler implements the logic for creating a milestone. It calls the Forgejo SDK's
// `CreateMilestone` function and returns the details of the newly created milestone.
func (impl CreateMilestoneImpl) Handler() mcp.ToolHandlerFor[CreateMilestoneParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args CreateMilestoneParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.CreateMilestoneOption{
Title: p.Title,
Description: p.Description,
}
// Set due date if provided
if !p.DueDate.IsZero() {
opt.Deadline = &p.DueDate
}
// Call SDK
milestone, _, err := impl.Client.CreateMilestone(p.Owner, p.Repo, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to create milestone: %w", err)
}
// Convert to our type and format
milestoneWrapper := &types.Milestone{Milestone: milestone}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: milestoneWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// EditMilestoneParams defines the parameters for the edit_milestone tool.
// It specifies the milestone to edit by ID and the fields to update.
type EditMilestoneParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// ID is the unique identifier of the milestone to edit.
ID int `json:"id"`
// Title is the new title for the milestone.
Title string `json:"title,omitempty"`
// Description is the new markdown description for the milestone.
Description string `json:"description,omitempty"`
// DueDate is the new optional due date for the milestone.
DueDate time.Time `json:"due_date,omitempty"`
// State is the new state for the milestone (e.g., 'open', 'closed').
State string `json:"state,omitempty"`
}
// EditMilestoneImpl implements the MCP tool for editing an existing milestone.
// This is an idempotent operation that modifies a milestone's metadata using
// the Forgejo SDK.
type EditMilestoneImpl struct {
Client *tools.Client
}
// Definition describes the `edit_milestone` tool. It requires `owner`, `repo`, and
// the milestone `id`. It is marked as idempotent.
func (EditMilestoneImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "edit_milestone",
Title: "Edit Milestone",
Description: "Edit an existing milestone's title, description, due date, or state.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"id": {
Type: "integer",
Description: "Milestone ID",
},
"title": {
Type: "string",
Description: "New milestone title (optional)",
},
"description": {
Type: "string",
Description: "New milestone description (optional)",
},
"due_date": {
Type: "string",
Description: "New milestone due date in ISO 8601 format (e.g., '2024-12-31T23:59:59Z') (optional)",
Format: "date-time",
},
"state": {
Type: "string",
Description: "New milestone state: 'open' or 'closed' (optional)",
Enum: []any{"open", "closed"},
},
},
Required: []string{"owner", "repo", "id"},
},
}
}
// Handler implements the logic for editing a milestone. It calls the Forgejo SDK's
// `EditMilestone` function. It will return an error if the milestone ID is not found.
func (impl EditMilestoneImpl) Handler() mcp.ToolHandlerFor[EditMilestoneParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args EditMilestoneParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.EditMilestoneOption{}
if p.Title != "" {
opt.Title = p.Title
}
if p.Description != "" {
opt.Description = &p.Description
}
if p.State != "" {
state := forgejo.StateType(p.State)
opt.State = &state
}
if !p.DueDate.IsZero() {
opt.Deadline = &p.DueDate
}
// Call SDK
milestone, _, err := impl.Client.EditMilestone(p.Owner, p.Repo, int64(p.ID), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to edit milestone: %w", err)
}
// Convert to our type and format
milestoneWrapper := &types.Milestone{Milestone: milestone}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: milestoneWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// DeleteMilestoneParams defines the parameters for the delete_milestone tool.
// It specifies the milestone to be deleted by its ID.
type DeleteMilestoneParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// ID is the unique identifier of the milestone to delete.
ID int `json:"id"`
}
// DeleteMilestoneImpl implements the destructive MCP tool for deleting a milestone.
// This is an idempotent but irreversible operation that removes a milestone from
// a repository using the Forgejo SDK.
type DeleteMilestoneImpl struct {
Client *tools.Client
}
// Definition describes the `delete_milestone` tool. It requires `owner`, `repo`,
// and the milestone `id`. It is marked as a destructive operation to ensure
// clients can warn the user before execution.
func (DeleteMilestoneImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "delete_milestone",
Title: "Delete Milestone",
Description: "Delete a milestone from a repository.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(true),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"id": {
Type: "integer",
Description: "Milestone ID to delete",
},
},
Required: []string{"owner", "repo", "id"},
},
}
}
// Handler implements the logic for deleting a milestone. It calls the Forgejo SDK's
// `DeleteMilestone` function. On success, it returns a simple text confirmation.
// It will return an error if the milestone does not exist.
func (impl DeleteMilestoneImpl) Handler() mcp.ToolHandlerFor[DeleteMilestoneParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteMilestoneParams) (*mcp.CallToolResult, any, error) {
p := args
// Call SDK
_, err := impl.Client.DeleteMilestone(p.Owner, p.Repo, int64(p.ID))
if err != nil {
return nil, nil, fmt.Errorf("failed to delete milestone: %w", err)
}
// Return success message
emptyResponse := types.EmptyResponse{}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: emptyResponse.ToMarkdown(),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/release/crud.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package release
import (
"context"
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListReleasesParams defines the parameters for the list_releases tool.
// It specifies the owner and repository, with optional pagination.
type ListReleasesParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Page is the page number for pagination.
Page int `json:"page,omitempty"`
// Limit is the number of releases to return per page.
Limit int `json:"limit,omitempty"`
}
// ListReleasesImpl implements the read-only MCP tool for listing repository releases.
// This is a safe, idempotent operation that uses the Forgejo SDK to fetch a
// paginated list of releases.
type ListReleasesImpl struct {
Client *tools.Client
}
// Definition describes the `list_releases` tool. It requires `owner` and `repo`
// and supports pagination. It is marked as a safe, read-only operation.
func (ListReleasesImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_releases",
Title: "List Releases",
Description: "List all releases in a repository. Returns release information including tag name, title, description, and assets.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"page": {
Type: "integer",
Description: "Page number for pagination (optional, defaults to 1)",
Minimum: tools.Float64Ptr(1),
},
"limit": {
Type: "integer",
Description: "Number of releases per page (optional, defaults to 10, max 50)",
Minimum: tools.Float64Ptr(1),
Maximum: tools.Float64Ptr(50),
},
},
Required: []string{"owner", "repo"},
},
}
}
// Handler implements the logic for listing releases. It calls the Forgejo SDK's
// `ListReleases` function and formats the results into a markdown list.
func (impl ListReleasesImpl) Handler() mcp.ToolHandlerFor[ListReleasesParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListReleasesParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.ListReleasesOptions{}
if p.Page > 0 {
opt.Page = p.Page
}
if p.Limit > 0 {
opt.PageSize = p.Limit
}
// Call SDK
releases, _, err := impl.Client.ListReleases(p.Owner, p.Repo, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to list releases: %w", err)
}
// Convert to our types and format
var content string
if len(releases) == 0 {
content = "No releases found in this repository."
} else {
// Convert releases to our type
releaseList := make(types.ReleaseList, len(releases))
for i, release := range releases {
releaseList[i] = &types.Release{Release: release}
}
content = fmt.Sprintf("Found %d releases\n\n%s",
len(releases), releaseList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// CreateReleaseParams defines the parameters for the create_release tool.
// It includes all necessary details for creating a new release.
type CreateReleaseParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// TagName is the name of the git tag for the release.
TagName string `json:"tag_name"`
// TargetCommitish is the branch or commit SHA to create the release from.
TargetCommitish string `json:"target_commitish,omitempty"`
// Name is the title of the release.
Name string `json:"name"`
// Body is the markdown description of the release.
Body string `json:"body,omitempty"`
// Draft indicates whether the release is a draft.
Draft bool `json:"draft,omitempty"`
// Prerelease indicates whether the release is a pre-release.
Prerelease bool `json:"prerelease,omitempty"`
}
// CreateReleaseImpl implements the MCP tool for creating a new release.
// This is a non-idempotent operation that creates a new git tag and a
// corresponding release object via the Forgejo SDK.
type CreateReleaseImpl struct {
Client *tools.Client
}
// Definition describes the `create_release` tool. It requires `owner`, `repo`,
// `tag_name`, and a `name`. It is not idempotent, as multiple calls will fail
// once the tag is created.
func (CreateReleaseImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "create_release",
Title: "Create Release",
Description: "Create a new release in a repository. Specify tag name, title, description, and other metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: false,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"tag_name": {
Type: "string",
Description: "Git tag name for this release",
},
"target_commitish": {
Type: "string",
Description: "Target branch or commit SHA (optional, defaults to default branch)",
},
"name": {
Type: "string",
Description: "Release title",
},
"body": {
Type: "string",
Description: "Release description (markdown supported) (optional)",
},
"draft": {
Type: "boolean",
Description: "Whether this is a draft release (optional, defaults to false)",
},
"prerelease": {
Type: "boolean",
Description: "Whether this is a prerelease (optional, defaults to false)",
},
},
Required: []string{"owner", "repo", "tag_name", "name"},
},
}
}
// Handler implements the logic for creating a release. It calls the Forgejo SDK's
// `CreateRelease` function. On success, it returns details of the new release.
func (impl CreateReleaseImpl) Handler() mcp.ToolHandlerFor[CreateReleaseParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args CreateReleaseParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.CreateReleaseOption{
TagName: p.TagName,
Target: p.TargetCommitish,
Title: p.Name,
Note: p.Body,
IsDraft: p.Draft,
IsPrerelease: p.Prerelease,
}
// Call SDK
release, _, err := impl.Client.CreateRelease(p.Owner, p.Repo, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to create release: %w", err)
}
// Convert to our type and format
releaseWrapper := &types.Release{Release: release}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: releaseWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// EditReleaseParams defines the parameters for the edit_release tool.
// It specifies the release to edit by ID and the fields to update.
type EditReleaseParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// ID is the unique identifier of the release to edit.
ID int `json:"id"`
// TagName is the new git tag name for the release.
TagName string `json:"tag_name,omitempty"`
// TargetCommitish is the new target branch or commit SHA.
TargetCommitish string `json:"target_commitish,omitempty"`
// Name is the new title for the release.
Name string `json:"name,omitempty"`
// Body is the new markdown description for the release.
Body string `json:"body,omitempty"`
// Draft indicates whether the release should be marked as a draft.
Draft bool `json:"draft,omitempty"`
// Prerelease indicates whether the release should be marked as a pre-release.
Prerelease bool `json:"prerelease,omitempty"`
}
// EditReleaseImpl implements the MCP tool for editing an existing release.
// This is an idempotent operation that modifies the metadata of a release
// identified by its ID, using the Forgejo SDK.
type EditReleaseImpl struct {
Client *tools.Client
}
// Definition describes the `edit_release` tool. It requires `owner`, `repo`, and
// the release `id`. It is marked as idempotent, as multiple identical calls
// will result in the same state.
func (EditReleaseImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "edit_release",
Title: "Edit Release",
Description: "Edit an existing release's title, description, or other metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"id": {
Type: "integer",
Description: "Release ID",
},
"tag_name": {
Type: "string",
Description: "New git tag name for this release (optional)",
},
"target_commitish": {
Type: "string",
Description: "New target branch or commit SHA (optional)",
},
"name": {
Type: "string",
Description: "New release title (optional)",
},
"body": {
Type: "string",
Description: "New release description (markdown supported) (optional)",
},
"draft": {
Type: "boolean",
Description: "Whether this is a draft release (optional)",
},
"prerelease": {
Type: "boolean",
Description: "Whether this is a prerelease (optional)",
},
},
Required: []string{"owner", "repo", "id"},
},
}
}
// Handler implements the logic for editing a release. It calls the Forgejo SDK's
// `EditRelease` function. It will return an error if the release ID is not found.
func (impl EditReleaseImpl) Handler() mcp.ToolHandlerFor[EditReleaseParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args EditReleaseParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.EditReleaseOption{}
if p.TagName != "" {
opt.TagName = p.TagName
}
if p.TargetCommitish != "" {
opt.Target = p.TargetCommitish
}
if p.Name != "" {
opt.Title = p.Name
}
if p.Body != "" {
opt.Note = p.Body
}
// Note: For boolean fields, we need to check if they were explicitly set
// For now, we'll pass them directly assuming they're properly handled
opt.IsDraft = &p.Draft
opt.IsPrerelease = &p.Prerelease
// Call SDK
release, _, err := impl.Client.EditRelease(p.Owner, p.Repo, int64(p.ID), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to edit release: %w", err)
}
// Convert to our type and format
releaseWrapper := &types.Release{Release: release}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: releaseWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// DeleteReleaseParams defines the parameters for the delete_release tool.
// It specifies the release to be deleted by its ID.
type DeleteReleaseParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// ID is the unique identifier of the release to delete.
ID int `json:"id"`
}
// DeleteReleaseImpl implements the destructive MCP tool for deleting a release.
// This is an idempotent but irreversible operation that removes both the release
// object and its associated git tag. It uses the Forgejo SDK.
type DeleteReleaseImpl struct {
Client *tools.Client
}
// Definition describes the `delete_release` tool. It requires `owner`, `repo`,
// and the release `id`. It is marked as a destructive operation to ensure
// clients can warn the user before execution.
func (DeleteReleaseImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "delete_release",
Title: "Delete Release",
Description: "Delete a release from a repository.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(true),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"id": {
Type: "integer",
Description: "Release ID to delete",
},
},
Required: []string{"owner", "repo", "id"},
},
}
}
// Handler implements the logic for deleting a release. It calls the Forgejo SDK's
// `DeleteRelease` function. On success, it returns a simple text confirmation.
// It will return an error if the release does not exist.
func (impl DeleteReleaseImpl) Handler() mcp.ToolHandlerFor[DeleteReleaseParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args DeleteReleaseParams) (*mcp.CallToolResult, any, error) {
p := args
// Call SDK
_, err := impl.Client.DeleteRelease(p.Owner, p.Repo, int64(p.ID))
if err != nil {
return nil, nil, fmt.Errorf("failed to delete release: %w", err)
}
// Return success message
emptyResponse := types.EmptyResponse{}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: emptyResponse.ToMarkdown(),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/repo/list.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package repo
import (
"context"
"fmt"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// SearchRepositoriesParams defines the parameters for the search_repositories tool.
// It includes the search query and various options for filtering and sorting.
type SearchRepositoriesParams struct {
// Q is the search query string.
Q string `json:"q"`
// Topic indicates whether to search in repository topics.
Topic bool `json:"topic,omitempty"`
// IncludeDesc indicates whether to include repository descriptions in the search.
IncludeDesc bool `json:"include_desc,omitempty"`
// Sort specifies the sort order for the results.
Sort string `json:"sort,omitempty"`
// Order specifies the sort direction (asc or desc).
Order string `json:"order,omitempty"`
// Page is the page number for pagination.
Page int `json:"page,omitempty"`
// Limit is the number of repositories to return per page.
Limit int `json:"limit,omitempty"`
}
// SearchRepositoriesImpl implements the read-only MCP tool for searching repositories.
// This operation is safe, idempotent, and uses the Forgejo SDK to find repositories
// across the entire Forgejo instance based on a query string.
type SearchRepositoriesImpl struct {
Client *tools.Client
}
// Definition describes the `search_repositories` tool. It requires a search query `q`
// and supports various optional parameters for sorting and pagination. It is
// marked as a safe, read-only operation.
func (SearchRepositoriesImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "search_repositories",
Title: "Search Repositories",
Description: "Search for repositories across the Forgejo instance. Returns repository information including name, description, and metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"q": {
Type: "string",
Description: "Search query string",
},
"topic": {
Type: "boolean",
Description: "Whether to search in repository topics (optional, defaults to true)",
},
"include_desc": {
Type: "boolean",
Description: "Whether to include repository descriptions in search (optional, defaults to true)",
},
"sort": {
Type: "string",
Description: "Sort order: 'alpha', 'created', 'updated', 'size', 'id' (optional, defaults to 'alpha')",
Enum: []any{"alpha", "created", "updated", "size", "id"},
},
"order": {
Type: "string",
Description: "Sort direction: 'asc' or 'desc' (optional, defaults to 'asc')",
Enum: []any{"asc", "desc"},
},
"page": {
Type: "integer",
Description: "Page number for pagination (optional, defaults to 1)",
Minimum: tools.Float64Ptr(1),
},
"limit": {
Type: "integer",
Description: "Number of repositories per page (optional, defaults to 10, max 50)",
Minimum: tools.Float64Ptr(1),
Maximum: tools.Float64Ptr(50),
},
},
Required: []string{"q"},
},
}
}
// Handler implements the logic for searching repositories. It calls the Forgejo SDK's
// `SearchRepos` function and formats the results into a markdown list.
func (impl SearchRepositoriesImpl) Handler() mcp.ToolHandlerFor[SearchRepositoriesParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args SearchRepositoriesParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.SearchRepoOptions{
Keyword: p.Q,
}
if p.Topic {
opt.KeywordIsTopic = p.Topic
}
if p.IncludeDesc {
opt.KeywordInDescription = p.IncludeDesc
}
if p.Sort != "" {
opt.Sort = p.Sort
}
if p.Order != "" {
opt.Order = p.Order
}
if p.Page > 0 {
opt.Page = p.Page
}
if p.Limit > 0 {
opt.PageSize = p.Limit
}
// Call SDK
repos, _, err := impl.Client.SearchRepos(opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to search repositories: %w", err)
}
// Convert to our types and format
var content string
if len(repos) == 0 {
content = "No repositories found matching the search criteria."
} else {
// Convert repos to our type
repoList := make(types.RepositoryList, len(repos))
for i, repo := range repos {
repoList[i] = &types.Repository{Repository: repo}
}
content = fmt.Sprintf("Found %d repositories\n\n%s",
len(repos), repoList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// ListMyRepositoriesParams defines the parameters for the list_my_repositories tool.
// It allows filtering and sorting of the authenticated user's repositories.
type ListMyRepositoriesParams struct {
// Affiliation filters repositories by the user's role.
Affiliation string `json:"affiliation,omitempty"`
// Visibility filters repositories by their public or private status.
Visibility string `json:"visibility,omitempty"`
// Sort specifies the sort order for the results.
Sort string `json:"sort,omitempty"`
// Direction specifies the sort direction (asc or desc).
Direction string `json:"direction,omitempty"`
// Page is the page number for pagination.
Page int `json:"page,omitempty"`
// Limit is the number of repositories to return per page.
Limit int `json:"limit,omitempty"`
}
// ListMyRepositoriesImpl implements the read-only MCP tool for listing the
// authenticated user's repositories. This is a safe, idempotent operation that
// uses the Forgejo SDK.
type ListMyRepositoriesImpl struct {
Client *tools.Client
}
// Definition describes the `list_my_repositories` tool. It supports optional
// parameters for filtering and sorting, and is marked as a safe, read-only operation.
func (ListMyRepositoriesImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_my_repositories",
Title: "List My Repositories",
Description: "List repositories owned by the authenticated user. Returns repository information including name, description, and metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"affiliation": {
Type: "string",
Description: "Repository affiliation filter: 'owner', 'collaborator', 'organization_member', or 'all' (optional, defaults to 'all')",
Enum: []any{"owner", "collaborator", "organization_member", "all"},
},
"visibility": {
Type: "string",
Description: "Repository visibility filter: 'all', 'public', 'private' (optional, defaults to 'all')",
Enum: []any{"all", "public", "private"},
},
"sort": {
Type: "string",
Description: "Sort order: 'created', 'updated', 'pushed', 'full_name' (optional, defaults to 'full_name')",
Enum: []any{"created", "updated", "pushed", "full_name"},
},
"direction": {
Type: "string",
Description: "Sort direction: 'asc' or 'desc' (optional, defaults to 'asc')",
Enum: []any{"asc", "desc"},
},
"page": {
Type: "integer",
Description: "Page number for pagination (optional, defaults to 1)",
Minimum: tools.Float64Ptr(1),
},
"limit": {
Type: "integer",
Description: "Number of repositories per page (optional, defaults to 20, max 50)",
Minimum: tools.Float64Ptr(1),
Maximum: tools.Float64Ptr(50),
},
},
Required: []string{},
},
}
}
// Handler implements the logic for listing the user's repositories. It calls the
// Forgejo SDK's `ListMyRepos` function and formats the results into a markdown list.
func (impl ListMyRepositoriesImpl) Handler() mcp.ToolHandlerFor[ListMyRepositoriesParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListMyRepositoriesParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.ListReposOptions{}
// Note: ListReposOptions is quite limited in the SDK
// Many filtering options are not available
if p.Page > 0 {
opt.Page = p.Page
}
if p.Limit > 0 {
opt.PageSize = p.Limit
}
// Call SDK
repos, _, err := impl.Client.ListMyRepos(opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to list my repositories: %w", err)
}
// Convert to our types and format
var content string
if len(repos) == 0 {
content = "No repositories found for the authenticated user."
} else {
// Convert repos to our type
repoList := make(types.RepositoryList, len(repos))
for i, repo := range repos {
repoList[i] = &types.Repository{Repository: repo}
}
content = fmt.Sprintf("Found %d repositories\n\n%s",
len(repos), repoList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// ListOrgRepositoriesParams defines the parameters for the list_org_repositories tool.
// It specifies the organization and allows for filtering and sorting.
type ListOrgRepositoriesParams struct {
// Org is the name of the organization.
Org string `json:"org"`
// Type filters repositories by their type (e.g., forks, sources).
Type string `json:"type,omitempty"`
// Sort specifies the sort order for the results.
Sort string `json:"sort,omitempty"`
// Direction specifies the sort direction (asc or desc).
Direction string `json:"direction,omitempty"`
// Page is the page number for pagination.
Page int `json:"page,omitempty"`
// Limit is the number of repositories to return per page.
Limit int `json:"limit,omitempty"`
}
// ListOrgRepositoriesImpl implements the read-only MCP tool for listing an
// organization's repositories. This is a safe, idempotent operation that uses
// the Forgejo SDK.
type ListOrgRepositoriesImpl struct {
Client *tools.Client
}
// Definition describes the `list_org_repositories` tool. It requires an `org` name
// and supports optional parameters for filtering and sorting. It is marked as a
// safe, read-only operation.
func (ListOrgRepositoriesImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_org_repositories",
Title: "List Organization Repositories",
Description: "List repositories owned by a specific organization. Returns repository information including name, description, and metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"org": {
Type: "string",
Description: "Organization name",
},
"type": {
Type: "string",
Description: "Repository type filter: 'all', 'public', 'private', 'forks', 'sources', 'member' (optional, defaults to 'all')",
Enum: []any{"all", "public", "private", "forks", "sources", "member"},
},
"sort": {
Type: "string",
Description: "Sort order: 'created', 'updated', 'pushed', 'full_name' (optional, defaults to 'created')",
Enum: []any{"created", "updated", "pushed", "full_name"},
},
"direction": {
Type: "string",
Description: "Sort direction: 'asc' or 'desc' (optional, defaults to 'desc')",
Enum: []any{"asc", "desc"},
},
"page": {
Type: "integer",
Description: "Page number for pagination (optional, defaults to 1)",
Minimum: tools.Float64Ptr(1),
},
"limit": {
Type: "integer",
Description: "Number of repositories per page (optional, defaults to 20, max 50)",
Minimum: tools.Float64Ptr(1),
Maximum: tools.Float64Ptr(50),
},
},
Required: []string{"org"},
},
}
}
// Handler implements the logic for listing organization repositories. It calls the
// Forgejo SDK's `ListOrgRepos` function and formats the results into a markdown list.
func (impl ListOrgRepositoriesImpl) Handler() mcp.ToolHandlerFor[ListOrgRepositoriesParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListOrgRepositoriesParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.ListOrgReposOptions{}
// Note: ListOrgReposOptions is quite limited in the SDK
// Type filtering is not available
if p.Page > 0 {
opt.Page = p.Page
}
if p.Limit > 0 {
opt.PageSize = p.Limit
}
// Call SDK
repos, _, err := impl.Client.ListOrgRepos(p.Org, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to list organization repositories: %w", err)
}
// Convert to our types and format
var content string
if len(repos) == 0 {
content = fmt.Sprintf("No repositories found for organization '%s'.", p.Org)
} else {
// Convert repos to our type
repoList := make(types.RepositoryList, len(repos))
for i, repo := range repos {
repoList[i] = &types.Repository{Repository: repo}
}
content = fmt.Sprintf("Found %d repositories for organization '%s'\n\n%s",
len(repos), p.Org, repoList.ToMarkdown())
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// GetRepositoryParams defines the parameters for the get_repository tool.
// It specifies the owner and repository name to retrieve.
type GetRepositoryParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
}
// GetRepositoryImpl implements the read-only MCP tool for fetching detailed
// information about a single repository. This is a safe, idempotent operation
// that uses the Forgejo SDK.
type GetRepositoryImpl struct {
Client *tools.Client
}
// Definition describes the `get_repository` tool. It requires `owner` and `repo`
// as parameters and is marked as a safe, read-only operation.
func (GetRepositoryImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "get_repository",
Title: "Get Repository Information",
Description: "Get detailed information about a specific repository, including description, stats, permissions, and metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
},
Required: []string{"owner", "repo"},
},
}
}
// Handler implements the logic for fetching repository details. It calls the
// Forgejo SDK's `GetRepo` function and formats the full repository object into
// a detailed markdown view.
func (impl GetRepositoryImpl) Handler() mcp.ToolHandlerFor[GetRepositoryParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args GetRepositoryParams) (*mcp.CallToolResult, any, error) {
p := args
// Call SDK
repo, _, err := impl.Client.GetRepo(p.Owner, p.Repo)
if err != nil {
return nil, nil, fmt.Errorf("failed to get repository: %w", err)
}
// Convert to our type and format
repoWrapper := &types.Repository{Repository: repo}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: repoWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/issue/crud.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package issue
import (
"context"
"fmt"
"strings"
"time"
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListRepoIssuesParams defines the parameters for the list_repo_issues tool.
// It includes extensive options for filtering, sorting, and paginating issues.
type ListRepoIssuesParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// State filters issues by their state (e.g., 'open', 'closed').
State string `json:"state,omitempty"`
// Labels is a comma-separated list of label names to filter by.
Labels string `json:"labels,omitempty"`
// Milestones is a comma-separated list of milestone names to filter by.
Milestones string `json:"milestones,omitempty"`
// Assignees is a comma-separated list of usernames to filter by assignee.
Assignees string `json:"assignees,omitempty"`
// Q is a search query string to filter issues by.
Q string `json:"q,omitempty"`
// Sort specifies the sort order for the results.
Sort string `json:"sort,omitempty"`
// Order specifies the sort direction (asc or desc).
Order string `json:"order,omitempty"`
// Page is the page number for pagination.
Page int `json:"page,omitempty"`
// Limit is the number of issues to return per page.
Limit int `json:"limit,omitempty"`
// Since is a timestamp in RFC 3339 format to show only issues updated after this time.
Since *string `json:"since,omitempty"`
// Before is a timestamp in RFC 3339 format to show only issues updated before this time.
Before *string `json:"before,omitempty"`
}
// ListRepoIssuesImpl implements the read-only MCP tool for listing repository issues.
// This is a safe, idempotent operation that uses the Forgejo SDK to fetch a list
// of issues with powerful filtering and sorting capabilities.
type ListRepoIssuesImpl struct {
Client *tools.Client
}
// Definition describes the `list_repo_issues` tool. It requires `owner` and `repo`
// and supports a rich set of optional parameters for filtering and sorting.
// It is marked as a safe, read-only operation.
func (ListRepoIssuesImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_repo_issues",
Title: "List Repository Issues",
Description: "List issues in a repository with optional filtering by state, labels, milestones, assignees, and search terms.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"state": {
Type: "string",
Description: "Issue state filter: 'open', 'closed', or 'all' (optional, defaults to 'open')",
Enum: []any{"open", "closed", "all"},
},
"labels": {
Type: "string",
Description: "Comma-separated list of label names to filter by (optional)",
},
"milestones": {
Type: "string",
Description: "Comma-separated list of milestone names or IDs to filter by (optional)",
},
"assignees": {
Type: "string",
Description: "Comma-separated list of usernames to filter by assignee (optional)",
},
"q": {
Type: "string",
Description: "Search query string to filter issues (optional)",
},
"sort": {
Type: "string",
Description: "Sort field: 'created', 'updated', 'comments' (optional, defaults to 'created')",
Enum: []any{"created", "updated", "comments"},
},
"order": {
Type: "string",
Description: "Sort order: 'asc' or 'desc' (optional, defaults to 'desc')",
Enum: []any{"asc", "desc"},
},
"page": {
Type: "integer",
Description: "Page number for pagination (optional, defaults to 1)",
Minimum: tools.Float64Ptr(1),
},
"limit": {
Type: "integer",
Description: "Number of issues per page (optional, defaults to 20, max 50)",
Minimum: tools.Float64Ptr(1),
Maximum: tools.Float64Ptr(50),
},
"since": {
Type: "string",
Description: "Only show items updated after this time (RFC 3339 format, optional)",
Format: "date-time",
},
"before": {
Type: "string",
Description: "Only show items updated before this time (RFC 3339 format, optional)",
Format: "date-time",
},
},
Required: []string{"owner", "repo"},
},
}
}
// Handler implements the logic for listing issues. It calls the Forgejo SDK's
// `ListRepoIssues` function with the provided filters and formats the results
// into a markdown table.
func (impl ListRepoIssuesImpl) Handler() mcp.ToolHandlerFor[ListRepoIssuesParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListRepoIssuesParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.ListIssueOption{}
if p.State != "" {
opt.State = forgejo.StateType(p.State)
}
if p.Labels != "" {
opt.Labels = strings.Split(p.Labels, ",")
}
if p.Milestones != "" {
opt.Milestones = strings.Split(p.Milestones, ",")
}
if p.Q != "" {
opt.KeyWord = p.Q
}
if p.Assignees != "" {
opt.AssignedBy = p.Assignees
}
if p.Page > 0 {
opt.Page = p.Page
}
if p.Limit > 0 {
opt.ListOptions.PageSize = p.Limit
}
// Handle time-based filters
if p.Since != nil {
since, err := time.Parse(time.RFC3339, *p.Since)
if err != nil {
return nil, nil, fmt.Errorf("invalid since timestamp format (expected RFC 3339): %w", err)
}
opt.Since = since
}
if p.Before != nil {
before, err := time.Parse(time.RFC3339, *p.Before)
if err != nil {
return nil, nil, fmt.Errorf("invalid before timestamp format (expected RFC 3339): %w", err)
}
opt.Before = before
}
// Call SDK
issues, _, err := impl.Client.ListRepoIssues(p.Owner, p.Repo, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to list issues: %w", err)
}
// Convert to our types and format
issueList := types.IssueList(issues)
content := fmt.Sprintf("Found %d issues\n\n%s", len(issues), issueList.ToMarkdown())
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// GetIssueParams defines the parameters for the get_issue tool.
// It specifies the issue to retrieve by its owner, repository, and index.
type GetIssueParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
}
// GetIssueImpl implements the read-only MCP tool for fetching a single issue.
// This is a safe, idempotent operation that uses the Forgejo SDK to retrieve
// detailed information about a specific issue.
type GetIssueImpl struct {
Client *tools.Client
}
// Definition describes the `get_issue` tool. It requires `owner`, `repo`, and
// the issue `index`. It is marked as a safe, read-only operation.
func (GetIssueImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "get_issue",
Title: "Get Issue Details",
Description: "Get detailed information about a specific issue, including title, body, state, assignees, labels, and metadata.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
},
Required: []string{"owner", "repo", "index"},
},
}
}
// Handler implements the logic for fetching an issue. It calls the Forgejo SDK's
// `GetIssue` function and formats the result into a detailed markdown view.
// It will return an error if the issue is not found.
func (impl GetIssueImpl) Handler() mcp.ToolHandlerFor[GetIssueParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args GetIssueParams) (*mcp.CallToolResult, any, error) {
p := args
// Call SDK
issue, _, err := impl.Client.GetIssue(p.Owner, p.Repo, int64(p.Index))
if err != nil {
return nil, nil, fmt.Errorf("failed to get issue: %w", err)
}
// Convert to our type and format
issueWrapper := &types.Issue{Issue: issue}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: issueWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// CreateIssueParams defines the parameters for the create_issue tool.
// It includes the title, body, and optional metadata for the new issue.
type CreateIssueParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Title is the title of the new issue.
Title string `json:"title"`
// Body is the markdown content of the issue.
Body string `json:"body"`
// Assignees is a slice of usernames to assign to the issue.
Assignees []string `json:"assignees,omitempty"`
// Milestone is the ID of a milestone to assign to the issue.
Milestone int `json:"milestone,omitempty"`
// Labels is a slice of label IDs to assign to the issue.
Labels []int `json:"labels,omitempty"`
// DueDate is the optional due date for the issue.
DueDate time.Time `json:"due_date,omitempty"`
}
// CreateIssueImpl implements the MCP tool for creating a new issue.
// This is a non-idempotent operation that creates a new issue using the Forgejo SDK.
type CreateIssueImpl struct {
Client *tools.Client
}
// Definition describes the `create_issue` tool. It requires `owner`, `repo`,
// a `title`, and a `body`. It is not idempotent, as multiple calls will create
// multiple identical issues.
func (CreateIssueImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "create_issue",
Title: "Create Issue",
Description: "Create a new issue in a repository. Specify title, body, and optional metadata like labels, assignees, milestone.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: false,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"title": {
Type: "string",
Description: "Issue title",
},
"body": {
Type: "string",
Description: "Issue body content (markdown supported)",
},
"assignees": {
Type: "array",
Items: &jsonschema.Schema{
Type: "string",
},
Description: "Array of usernames to assign to this issue (optional)",
},
"milestone": {
Type: "integer",
Description: "Milestone ID to assign to this issue (optional)",
},
"labels": {
Type: "array",
Items: &jsonschema.Schema{
Type: "integer",
},
Description: "Array of label IDs to assign to this issue (optional)",
},
"due_date": {
Type: "string",
Description: "Issue due date in ISO 8601 format (e.g., '2024-12-31T23:59:59Z') (optional)",
Format: "date-time",
},
},
Required: []string{"owner", "repo", "title", "body"},
},
}
}
// Handler implements the logic for creating an issue. It calls the Forgejo SDK's
// `CreateIssue` function and returns the details of the newly created issue.
func (impl CreateIssueImpl) Handler() mcp.ToolHandlerFor[CreateIssueParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args CreateIssueParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.CreateIssueOption{
Title: p.Title,
Body: p.Body,
Assignees: p.Assignees,
}
// Set milestone if provided
if p.Milestone > 0 {
opt.Milestone = int64(p.Milestone)
}
// Convert label IDs from int to int64
if len(p.Labels) > 0 {
opt.Labels = make([]int64, len(p.Labels))
for i, label := range p.Labels {
opt.Labels[i] = int64(label)
}
}
// Set due date if provided
if !p.DueDate.IsZero() {
opt.Deadline = &p.DueDate
}
// Call SDK
issue, _, err := impl.Client.CreateIssue(p.Owner, p.Repo, opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to create issue: %w", err)
}
// Convert to our type and format
issueWrapper := &types.Issue{Issue: issue}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: issueWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
// EditIssueParams defines the parameters for the edit_issue tool.
// It specifies the issue to edit by ID and the fields to update.
type EditIssueParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
// Title is the new title for the issue.
Title string `json:"title,omitempty"`
// Body is the new markdown content for the issue.
Body string `json:"body,omitempty"`
// State is the new state for the issue (e.g., 'open', 'closed').
State string `json:"state,omitempty"`
// Assignees is the new list of usernames to assign to the issue.
Assignees []string `json:"assignees,omitempty"`
// Milestone is the new milestone ID to assign to the issue.
Milestone int `json:"milestone,omitempty"`
// DueDate is the new optional due date for the issue.
DueDate time.Time `json:"due_date,omitempty"`
}
// EditIssueImpl implements the MCP tool for editing an existing issue.
// This is an idempotent operation that modifies an issue's metadata using the
// Forgejo SDK.
type EditIssueImpl struct {
Client *tools.Client
}
// Definition describes the `edit_issue` tool. It requires `owner`, `repo`, and the
// issue `index`. It is marked as idempotent.
func (EditIssueImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "edit_issue",
Title: "Edit Issue",
Description: "Edit an existing issue's title, body, state, assignees, milestone, or due date.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"title": {
Type: "string",
Description: "New issue title (optional)",
},
"body": {
Type: "string",
Description: "New issue body content (markdown supported) (optional)",
},
"state": {
Type: "string",
Description: "New issue state: 'open' or 'closed' (optional)",
Enum: []any{"open", "closed"},
},
"assignees": {
Type: "array",
Items: &jsonschema.Schema{
Type: "string",
},
Description: "Array of usernames to assign to this issue (optional)",
},
"milestone": {
Type: "integer",
Description: "Milestone ID to assign to this issue (optional)",
},
"due_date": {
Type: "string",
Description: "Issue due date in ISO 8601 format (e.g., '2024-12-31T23:59:59Z') (optional)",
Format: "date-time",
},
},
Required: []string{"owner", "repo", "index"},
},
}
}
// Handler implements the logic for editing an issue. It calls the Forgejo SDK's
// `EditIssue` function. It will return an error if the issue is not found.
func (impl EditIssueImpl) Handler() mcp.ToolHandlerFor[EditIssueParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args EditIssueParams) (*mcp.CallToolResult, any, error) {
p := args
// Build options for SDK call
opt := forgejo.EditIssueOption{
Assignees: p.Assignees,
}
// Set title if provided
if p.Title != "" {
opt.Title = p.Title
}
// Set body if provided
if p.Body != "" {
opt.Body = &p.Body
}
// Set state if provided
if p.State != "" {
state := forgejo.StateType(p.State)
opt.State = &state
}
// Set milestone if provided
if p.Milestone > 0 {
milestone := int64(p.Milestone)
opt.Milestone = &milestone
}
// Set due date if provided
if !p.DueDate.IsZero() {
opt.Deadline = &p.DueDate
}
// Call SDK
issue, _, err := impl.Client.EditIssue(p.Owner, p.Repo, int64(p.Index), opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to edit issue: %w", err)
}
// Convert to our type and format
issueWrapper := &types.Issue{Issue: issue}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: issueWrapper.ToMarkdown(),
},
},
}, nil, nil
}
}
```
--------------------------------------------------------------------------------
/tools/issue/dep.go:
--------------------------------------------------------------------------------
```go
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright © 2025 Ronmi Ren <[email protected]>
package issue
import (
"context"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/raohwork/forgejo-mcp/tools"
"github.com/raohwork/forgejo-mcp/types"
)
// ListIssueDependenciesParams defines the parameters for the list_issue_dependencies tool.
// It specifies the issue for which to list dependencies.
type ListIssueDependenciesParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
}
// ListIssueDependenciesImpl implements the read-only MCP tool for listing issue dependencies.
// This is a safe, idempotent operation. Note: This feature is not supported by the
// official Forgejo SDK and requires a custom HTTP implementation.
type ListIssueDependenciesImpl struct {
Client *tools.Client
}
// Definition describes the `list_issue_dependencies` tool. It requires `owner`, `repo`,
// and the issue `index`. It is marked as a safe, read-only operation.
func (ListIssueDependenciesImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_issue_dependencies",
Title: "List Issue Dependencies",
Description: "List all issues that must be closed before this issue can be closed. Shows dependency relationships where this issue is blocked by other issues.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
},
Required: []string{"owner", "repo", "index"},
},
}
}
// Handler implements the logic for listing issue dependencies. It performs a custom
// HTTP GET request to the `/repos/{owner}/{repo}/issues/{index}/dependencies`
// endpoint and formats the results into a markdown list.
func (impl ListIssueDependenciesImpl) Handler() mcp.ToolHandlerFor[ListIssueDependenciesParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListIssueDependenciesParams) (*mcp.CallToolResult, any, error) {
p := args
issues, err := impl.Client.MyListIssueDependencies(p.Owner, p.Repo, int64(p.Index))
if err != nil {
return nil, nil, fmt.Errorf("failed to list dependencies: %w", err)
}
dependencies := types.IssueDependencyList(issues)
content := fmt.Sprintf("## Issues that block #%d\n\n%s", p.Index, dependencies.ToMarkdown())
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// AddIssueDependencyParams defines the parameters for the add_issue_dependency tool.
// It specifies the two issues to link in a dependency relationship.
type AddIssueDependencyParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number of the dependent issue.
Index int `json:"index"`
// DependencyIndex is the issue number of the issue that `Index` will depend on.
DependencyIndex int `json:"dependency_index"`
}
// AddIssueDependencyImpl implements the MCP tool for adding a dependency to an issue.
// This is an idempotent operation. Note: This feature is not supported by the
// official Forgejo SDK and requires a custom HTTP implementation.
type AddIssueDependencyImpl struct {
Client *tools.Client
}
// Definition describes the `add_issue_dependency` tool. It requires the `index` of
// the dependent issue and the `dependency_index` of the issue it depends on.
// It is marked as idempotent.
func (AddIssueDependencyImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "add_issue_dependency",
Title: "Add Issue Dependency",
Description: "Add a dependency relationship where this issue depends on another issue. The dependency must be closed before this issue can be closed.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"dependency_index": {
Type: "integer",
Description: "Index of the issue this issue depends on",
},
},
Required: []string{"owner", "repo", "index", "dependency_index"},
},
}
}
// Handler implements the logic for adding an issue dependency. It performs a custom
// HTTP POST request to the `/repos/{owner}/{repo}/issues/{index}/dependencies`
// endpoint. It will return an error if either issue cannot be found.
func (impl AddIssueDependencyImpl) Handler() mcp.ToolHandlerFor[AddIssueDependencyParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args AddIssueDependencyParams) (*mcp.CallToolResult, any, error) {
p := args
dependency := types.MyIssueMeta{
Owner: p.Owner,
Name: p.Repo,
Index: int64(p.DependencyIndex),
}
_, err := impl.Client.MyAddIssueDependency(p.Owner, p.Repo, int64(p.Index), dependency)
if err != nil {
return nil, nil, fmt.Errorf("failed to add dependency: %w", err)
}
response := types.EmptyResponse{}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("Issue #%d now blocks issue #%d\n\n%s", p.DependencyIndex, p.Index, response.ToMarkdown()),
},
},
}, nil, nil
}
}
// RemoveIssueDependencyParams defines the parameters for the remove_issue_dependency tool.
// It specifies the two issues for which to remove the dependency relationship.
type RemoveIssueDependencyParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number of the dependent issue.
Index int `json:"index"`
// DependencyIndex is the issue number of the dependency to be removed.
DependencyIndex int `json:"dependency_index"`
}
// RemoveIssueDependencyImpl implements the destructive MCP tool for removing an issue dependency.
// This is an idempotent but destructive operation. Note: This feature is not supported
// by the official Forgejo SDK and requires a custom HTTP implementation.
type RemoveIssueDependencyImpl struct {
Client *tools.Client
}
// Definition describes the `remove_issue_dependency` tool. It requires the `index` of
// the dependent issue and the `dependency_index` to remove. It is marked as a
// destructive operation.
func (RemoveIssueDependencyImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "remove_issue_dependency",
Title: "Remove Issue Dependency",
Description: "Remove a dependency relationship where this issue depends on another issue. This allows the issue to be closed independently.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(true),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"dependency_index": {
Type: "integer",
Description: "Index of the dependency issue to remove",
},
},
Required: []string{"owner", "repo", "index", "dependency_index"},
},
}
}
// Handler implements the logic for removing an issue dependency. It performs a custom
// HTTP DELETE request to the `/repos/{owner}/{repo}/issues/{index}/dependencies/{dependency_index}`
// endpoint. On success, it returns a simple text confirmation.
func (impl RemoveIssueDependencyImpl) Handler() mcp.ToolHandlerFor[RemoveIssueDependencyParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args RemoveIssueDependencyParams) (*mcp.CallToolResult, any, error) {
p := args
dependency := types.MyIssueMeta{
Owner: p.Owner,
Name: p.Repo,
Index: int64(p.DependencyIndex),
}
_, err := impl.Client.MyRemoveIssueDependency(p.Owner, p.Repo, int64(p.Index), dependency)
if err != nil {
return nil, nil, fmt.Errorf("failed to remove dependency: %w", err)
}
response := types.EmptyResponse{}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("Issue #%d no longer blocks issue #%d\n\n%s", p.DependencyIndex, p.Index, response.ToMarkdown()),
},
},
}, nil, nil
}
}
// ListIssueBlockingParams defines the parameters for the list_issue_blocking tool.
// It specifies the issue for which to list blocking relationships.
type ListIssueBlockingParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number.
Index int `json:"index"`
}
// ListIssueBlockingImpl implements the read-only MCP tool for listing issue blocking relationships.
// This is a safe, idempotent operation. Note: This feature is not supported by the
// official Forgejo SDK and requires a custom HTTP implementation.
type ListIssueBlockingImpl struct {
Client *tools.Client
}
// Definition describes the `list_issue_blocking` tool. It requires `owner`, `repo`,
// and the issue `index`. It is marked as a safe, read-only operation.
func (ListIssueBlockingImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "list_issue_blocking",
Title: "List Issue Blocking",
Description: "List all issues that are blocked by this issue, showing which issues cannot be closed until this issue is closed.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
},
Required: []string{"owner", "repo", "index"},
},
}
}
// Handler implements the logic for listing issue blocking relationships. It performs a custom
// HTTP GET request to the `/repos/{owner}/{repo}/issues/{index}/blocks`
// endpoint and formats the results into a markdown list.
func (impl ListIssueBlockingImpl) Handler() mcp.ToolHandlerFor[ListIssueBlockingParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args ListIssueBlockingParams) (*mcp.CallToolResult, any, error) {
p := args
issues, err := impl.Client.MyListIssueBlocking(p.Owner, p.Repo, int64(p.Index))
if err != nil {
return nil, nil, fmt.Errorf("failed to list blocking issues: %w", err)
}
blockingList := types.IssueBlockingList(issues)
content := fmt.Sprintf("## Issues blocked by #%d\n\n%s", p.Index, blockingList.ToMarkdown())
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: content,
},
},
}, nil, nil
}
}
// AddIssueBlockingParams defines the parameters for the add_issue_blocking tool.
// It specifies the two issues to link in a blocking relationship.
type AddIssueBlockingParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number of the blocking issue.
Index int `json:"index"`
// BlockedIndex is the issue number of the issue that will be blocked by `Index`.
BlockedIndex int `json:"blocked_index"`
}
// AddIssueBlockingImpl implements the MCP tool for adding a blocking relationship to an issue.
// This is an idempotent operation. Note: This feature is not supported by the
// official Forgejo SDK and requires a custom HTTP implementation.
type AddIssueBlockingImpl struct {
Client *tools.Client
}
// Definition describes the `add_issue_blocking` tool. It requires the `index` of
// the blocking issue and the `blocked_index` of the issue it will block.
// It is marked as idempotent.
func (AddIssueBlockingImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "add_issue_blocking",
Title: "Add Issue Blocking",
Description: "Add a blocking relationship where this issue blocks another issue. The blocked issue cannot be closed until this issue is closed first.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(false),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"blocked_index": {
Type: "integer",
Description: "Index of the issue that will be blocked by this issue",
},
},
Required: []string{"owner", "repo", "index", "blocked_index"},
},
}
}
// Handler implements the logic for adding an issue blocking relationship. It performs a custom
// HTTP POST request to the `/repos/{owner}/{repo}/issues/{index}/blocks`
// endpoint. It will return an error if either issue cannot be found.
func (impl AddIssueBlockingImpl) Handler() mcp.ToolHandlerFor[AddIssueBlockingParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args AddIssueBlockingParams) (*mcp.CallToolResult, any, error) {
p := args
blocked := types.MyIssueMeta{
Owner: p.Owner,
Name: p.Repo,
Index: int64(p.BlockedIndex),
}
_, err := impl.Client.MyAddIssueBlocking(p.Owner, p.Repo, int64(p.Index), blocked)
if err != nil {
return nil, nil, fmt.Errorf("failed to add blocking relationship: %w", err)
}
response := types.EmptyResponse{}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("Issue #%d now blocks issue #%d\n\n%s", p.Index, p.BlockedIndex, response.ToMarkdown()),
},
},
}, nil, nil
}
}
// RemoveIssueBlockingParams defines the parameters for the remove_issue_blocking tool.
// It specifies the two issues for which to remove the blocking relationship.
type RemoveIssueBlockingParams struct {
// Owner is the username or organization name that owns the repository.
Owner string `json:"owner"`
// Repo is the name of the repository.
Repo string `json:"repo"`
// Index is the issue number of the blocking issue.
Index int `json:"index"`
// BlockedIndex is the issue number of the blocked issue to be unblocked.
BlockedIndex int `json:"blocked_index"`
}
// RemoveIssueBlockingImpl implements the destructive MCP tool for removing an issue blocking relationship.
// This is an idempotent but destructive operation. Note: This feature is not supported
// by the official Forgejo SDK and requires a custom HTTP implementation.
type RemoveIssueBlockingImpl struct {
Client *tools.Client
}
// Definition describes the `remove_issue_blocking` tool. It requires the `index` of
// the blocking issue and the `blocked_index` to remove. It is marked as a
// destructive operation.
func (RemoveIssueBlockingImpl) Definition() *mcp.Tool {
return &mcp.Tool{
Name: "remove_issue_blocking",
Title: "Remove Issue Blocking",
Description: "Remove a blocking relationship where this issue blocks another issue. This allows the blocked issue to be closed independently.",
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: false,
DestructiveHint: tools.BoolPtr(true),
IdempotentHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization name)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"index": {
Type: "integer",
Description: "Issue index number",
},
"blocked_index": {
Type: "integer",
Description: "Index of the blocked issue to remove from blocking relationship",
},
},
Required: []string{"owner", "repo", "index", "blocked_index"},
},
}
}
// Handler implements the logic for removing an issue blocking relationship. It performs a custom
// HTTP DELETE request to the `/repos/{owner}/{repo}/issues/{index}/blocks/{blocked_index}`
// endpoint. On success, it returns a simple text confirmation.
func (impl RemoveIssueBlockingImpl) Handler() mcp.ToolHandlerFor[RemoveIssueBlockingParams, any] {
return func(ctx context.Context, req *mcp.CallToolRequest, args RemoveIssueBlockingParams) (*mcp.CallToolResult, any, error) {
p := args
blocked := types.MyIssueMeta{
Owner: p.Owner,
Name: p.Repo,
Index: int64(p.BlockedIndex),
}
_, err := impl.Client.MyRemoveIssueBlocking(p.Owner, p.Repo, int64(p.Index), blocked)
if err != nil {
return nil, nil, fmt.Errorf("failed to remove blocking relationship: %w", err)
}
response := types.EmptyResponse{}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{
Text: fmt.Sprintf("Issue #%d no longer blocks issue #%d\n\n%s", p.Index, p.BlockedIndex, response.ToMarkdown()),
},
},
}, nil, nil
}
}
```