Skip to main content
Glama
crud.go18 kB
// 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 <ronmi.ren@gmail.com> 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 } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/raohwork/forgejo-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server