#
tokens: 35554/50000 9/87 files (page 2/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 2. Use http://codebase.md/raohwork/forgejo-mcp?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
	}
}

```
Page 2/2FirstPrevNextLast