2 Commits

Author SHA1 Message Date
b3nw
c2fac7754c fix: resolve network mode SSE and logging issues
- Fixes 404 errors on /sse and /message endpoints in network mode.
- Updates dev container and go.mod to use a compatible Go version.
- Restores and improves startup logging for all transport modes.
2025-07-14 02:01:29 +00:00
b3nw
f714887d1c feat: add network mode for combined HTTP and SSE endpoints
Add new 'network' transport mode that serves both HTTP and SSE protocols
on the same port with different URL paths:
- HTTP endpoint: /mcp
- SSE endpoint: /sse

Changes:
- Add network mode to operation/operation.go using http.ServeMux routing
- Update cmd/cmd.go flag descriptions to include network option
- Update README.md with network mode documentation and examples
- Update config.json with network mode configuration examples

The network mode allows clients to choose between HTTP or SSE protocols
without requiring separate server instances or ports. This provides
better resource utilization and simpler deployment.

Backward compatibility maintained - all existing modes (stdio, http, sse)
work unchanged.

Based on v0.3.0 for clean upstream compatibility.
2025-07-13 21:54:56 +00:00
9 changed files with 94 additions and 948 deletions

View File

@@ -21,13 +21,13 @@ func init() {
&flagPkg.Mode,
"t",
"stdio",
"Transport type (stdio, sse or http)",
"Transport type (stdio, sse, http, or network)",
)
flag.StringVar(
&flagPkg.Mode,
"transport",
"stdio",
"Transport type (stdio, sse or http)",
"Transport type (stdio, sse, http, or network)",
)
flag.StringVar(
&host,

BIN
gitea-mcp-network Executable file

Binary file not shown.

BIN
gitea-mcp-v0.3.0 Executable file

Binary file not shown.

6
go.mod
View File

@@ -1,10 +1,12 @@
module gitea.com/gitea/gitea-mcp
go 1.24.0
go 1.23.0
toolchain go1.23.11
require (
code.gitea.io/sdk/gitea v0.21.0
github.com/mark3labs/mcp-go v0.32.0
github.com/mark3labs/mcp-go v0.30.0
go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)

4
go.sum
View File

@@ -20,8 +20,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mark3labs/mcp-go v0.30.0 h1:Taz7fiefkxY/l8jz1nA90V+WdM2eoMtlvwfWforVYbo=
github.com/mark3labs/mcp-go v0.30.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=

View File

@@ -1,455 +0,0 @@
package label
import (
"context"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
CreateLabelToolName = "create_label"
GetRepoLabelToolName = "get_repo_label"
ListRepoLabelsToolName = "list_repo_labels"
EditLabelToolName = "edit_label"
DeleteLabelToolName = "delete_label"
GetIssueLabelsToolName = "get_issue_labels"
AddIssueLabelsToolName = "add_issue_labels"
RemoveIssueLabelsToolName = "remove_issue_labels"
ReplaceIssueLabelsToolName = "replace_issue_labels"
)
var (
CreateLabelTool = mcp.NewTool(
CreateLabelToolName,
mcp.WithDescription("Create label"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("label name")),
mcp.WithString("color", mcp.Required(), mcp.Description("label color in hex format (without #)")),
mcp.WithString("description", mcp.Description("label description")),
)
GetRepoLabelTool = mcp.NewTool(
GetRepoLabelToolName,
mcp.WithDescription("Get repository label by ID"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
)
ListRepoLabelsTool = mcp.NewTool(
ListRepoLabelsToolName,
mcp.WithDescription("List repository labels"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
mcp.WithNumber("limit", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
)
EditLabelTool = mcp.NewTool(
EditLabelToolName,
mcp.WithDescription("Edit label"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
mcp.WithString("name", mcp.Description("label name")),
mcp.WithString("color", mcp.Description("label color in hex format (without #)")),
mcp.WithString("description", mcp.Description("label description")),
)
DeleteLabelTool = mcp.NewTool(
DeleteLabelToolName,
mcp.WithDescription("Delete label"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
)
GetIssueLabelsTool = mcp.NewTool(
GetIssueLabelsToolName,
mcp.WithDescription("Get labels for an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
)
AddIssueLabelsTool = mcp.NewTool(
AddIssueLabelsToolName,
mcp.WithDescription("Add labels to an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to add"), mcp.Items(map[string]interface{}{"type": "number"})),
)
RemoveIssueLabelsTool = mcp.NewTool(
RemoveIssueLabelsToolName,
mcp.WithDescription("Remove labels from an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to remove"), mcp.Items(map[string]interface{}{"type": "number"})),
)
ReplaceIssueLabelsTool = mcp.NewTool(
ReplaceIssueLabelsToolName,
mcp.WithDescription("Replace all labels on an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to set"), mcp.Items(map[string]interface{}{"type": "number"})),
)
)
func init() {
Tool.RegisterWrite(server.ServerTool{
Tool: CreateLabelTool,
Handler: CreateLabelFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetRepoLabelTool,
Handler: GetRepoLabelFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: ListRepoLabelsTool,
Handler: ListRepoLabelsFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: EditLabelTool,
Handler: EditLabelFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeleteLabelTool,
Handler: DeleteLabelFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetIssueLabelsTool,
Handler: GetIssueLabelsFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: AddIssueLabelsTool,
Handler: AddIssueLabelsFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: RemoveIssueLabelsTool,
Handler: RemoveIssueLabelsFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: ReplaceIssueLabelsTool,
Handler: ReplaceIssueLabelsFn,
})
}
func CreateLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("name is required"))
}
color, ok := req.GetArguments()["color"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("color is required"))
}
opt := gitea_sdk.CreateLabelOption{
Name: name,
Color: color,
}
if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = description
}
label, _, err := gitea.Client().CreateLabel(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/%v/label err: %v", owner, repo, err))
}
return to.TextResult(label)
}
func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("id is required"))
}
label, _, err := gitea.Client().GetRepoLabel(owner, repo, int64(id))
if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, int64(id), err))
}
return to.TextResult(label)
}
func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
page, ok := req.GetArguments()["page"].(float64)
if !ok {
page = 1
}
limit, ok := req.GetArguments()["limit"].(float64)
if !ok {
limit = 50
}
opt := gitea_sdk.ListLabelsOptions{
ListOptions: gitea_sdk.ListOptions{
Page: int(page),
PageSize: int(limit),
},
}
labels, _, err := gitea.Client().ListRepoLabels(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/labels err: %v", owner, repo, err))
}
return to.TextResult(labels)
}
func EditLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("id is required"))
}
opt := gitea_sdk.EditLabelOption{}
if name, ok := req.GetArguments()["name"].(string); ok {
opt.Name = &name
}
if color, ok := req.GetArguments()["color"].(string); ok {
opt.Color = &color
}
if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = &description
}
label, _, err := gitea.Client().EditLabel(owner, repo, int64(id), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, int64(id), err))
}
return to.TextResult(label)
}
func DeleteLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("id is required"))
}
_, err := gitea.Client().DeleteLabel(owner, repo, int64(id))
if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/%v/label/%v err: %v", owner, repo, int64(id), err))
}
return to.TextResult(map[string]interface{}{
"success": true,
"message": fmt.Sprintf("Label %d deleted successfully", int64(id)),
})
}
func GetIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
index, ok := req.GetArguments()["index"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("index is required"))
}
labels, _, err := gitea.Client().GetIssueLabels(owner, repo, int64(index), gitea_sdk.ListLabelsOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/labels err: %v", owner, repo, int64(index), err))
}
return to.TextResult(labels)
}
func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
index, ok := req.GetArguments()["index"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("index is required"))
}
labelsData, ok := req.GetArguments()["labels"].([]interface{})
if !ok {
return to.ErrorResult(fmt.Errorf("labels is required"))
}
labelIDs := make([]int64, len(labelsData))
for i, labelData := range labelsData {
if labelID, ok := labelData.(float64); ok {
labelIDs[i] = int64(labelID)
} else {
return to.ErrorResult(fmt.Errorf("invalid label ID at index %d", i))
}
}
opt := gitea_sdk.IssueLabelsOption{
Labels: labelIDs,
}
labels, _, err := gitea.Client().AddIssueLabels(owner, repo, int64(index), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issues/%v err: %v", owner, repo, int64(index), err))
}
return to.TextResult(labels)
}
func RemoveIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RemoveIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
index, ok := req.GetArguments()["index"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("index is required"))
}
labelsData, ok := req.GetArguments()["labels"].([]interface{})
if !ok {
return to.ErrorResult(fmt.Errorf("labels is required"))
}
labelIDs := make([]int64, len(labelsData))
for i, labelData := range labelsData {
if labelID, ok := labelData.(float64); ok {
labelIDs[i] = int64(labelID)
} else {
return to.ErrorResult(fmt.Errorf("invalid label ID at index %d", i))
}
}
var errors []string
for _, labelID := range labelIDs {
_, err := gitea.Client().DeleteIssueLabel(owner, repo, int64(index), labelID)
if err != nil {
errors = append(errors, fmt.Sprintf("failed to remove label %d: %v", labelID, err))
}
}
if len(errors) > 0 {
return to.ErrorResult(fmt.Errorf("remove labels from %v/%v/issues/%v errors: %v", owner, repo, int64(index), errors))
}
return to.TextResult(map[string]interface{}{
"success": true,
"message": fmt.Sprintf("Labels removed from issue %d successfully", int64(index)),
})
}
func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ReplaceIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
index, ok := req.GetArguments()["index"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("index is required"))
}
labelsData, ok := req.GetArguments()["labels"].([]interface{})
if !ok {
return to.ErrorResult(fmt.Errorf("labels is required"))
}
labelIDs := make([]int64, len(labelsData))
for i, labelData := range labelsData {
if labelID, ok := labelData.(float64); ok {
labelIDs[i] = int64(labelID)
} else {
return to.ErrorResult(fmt.Errorf("invalid label ID at index %d", i))
}
}
opt := gitea_sdk.IssueLabelsOption{
Labels: labelIDs,
}
labels, _, err := gitea.Client().ReplaceIssueLabels(owner, repo, int64(index), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issues/%v err: %v", owner, repo, int64(index), err))
}
return to.TextResult(labels)
}

View File

@@ -1,402 +0,0 @@
package milestone
import (
"context"
"fmt"
"time"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
CreateMilestoneToolName = "create_milestone"
GetMilestoneToolName = "get_milestone"
GetMilestoneByNameToolName = "get_milestone_by_name"
ListRepoMilestonesToolName = "list_repo_milestones"
EditMilestoneToolName = "edit_milestone"
EditMilestoneByNameToolName = "edit_milestone_by_name"
DeleteMilestoneToolName = "delete_milestone"
DeleteMilestoneByNameToolName = "delete_milestone_by_name"
)
var (
CreateMilestoneTool = mcp.NewTool(
CreateMilestoneToolName,
mcp.WithDescription("Create milestone"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("title", mcp.Required(), mcp.Description("milestone title")),
mcp.WithString("description", mcp.Description("milestone description")),
mcp.WithString("due_date", mcp.Description("milestone due date in RFC3339 format")),
mcp.WithString("state", mcp.Description("milestone state (open or closed)"), mcp.DefaultString("open")),
)
GetMilestoneTool = mcp.NewTool(
GetMilestoneToolName,
mcp.WithDescription("Get milestone by ID"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone ID")),
)
GetMilestoneByNameTool = mcp.NewTool(
GetMilestoneByNameToolName,
mcp.WithDescription("Get milestone by name"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("milestone name")),
)
ListRepoMilestonesTool = mcp.NewTool(
ListRepoMilestonesToolName,
mcp.WithDescription("List repository milestones"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("state", mcp.Description("milestone state (open, closed, all)"), mcp.DefaultString("open")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
mcp.WithNumber("limit", mcp.Description("page size"), mcp.DefaultNumber(10), mcp.Min(1)),
)
EditMilestoneTool = mcp.NewTool(
EditMilestoneToolName,
mcp.WithDescription("Edit milestone by ID"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone ID")),
mcp.WithString("title", mcp.Description("milestone title")),
mcp.WithString("description", mcp.Description("milestone description")),
mcp.WithString("due_date", mcp.Description("milestone due date in RFC3339 format")),
mcp.WithString("state", mcp.Description("milestone state (open or closed)")),
)
EditMilestoneByNameTool = mcp.NewTool(
EditMilestoneByNameToolName,
mcp.WithDescription("Edit milestone by name"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("milestone name")),
mcp.WithString("title", mcp.Description("milestone title")),
mcp.WithString("description", mcp.Description("milestone description")),
mcp.WithString("due_date", mcp.Description("milestone due date in RFC3339 format")),
mcp.WithString("state", mcp.Description("milestone state (open or closed)")),
)
DeleteMilestoneTool = mcp.NewTool(
DeleteMilestoneToolName,
mcp.WithDescription("Delete milestone by ID"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone ID")),
)
DeleteMilestoneByNameTool = mcp.NewTool(
DeleteMilestoneByNameToolName,
mcp.WithDescription("Delete milestone by name"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("milestone name")),
)
)
func init() {
Tool.RegisterWrite(server.ServerTool{
Tool: CreateMilestoneTool,
Handler: CreateMilestoneFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetMilestoneTool,
Handler: GetMilestoneFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetMilestoneByNameTool,
Handler: GetMilestoneByNameFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: ListRepoMilestonesTool,
Handler: ListRepoMilestonesFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: EditMilestoneTool,
Handler: EditMilestoneFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: EditMilestoneByNameTool,
Handler: EditMilestoneByNameFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeleteMilestoneTool,
Handler: DeleteMilestoneFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeleteMilestoneByNameTool,
Handler: DeleteMilestoneByNameFn,
})
}
func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateMilestoneFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
title, ok := req.GetArguments()["title"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("title is required"))
}
opt := gitea_sdk.CreateMilestoneOption{
Title: title,
}
if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = description
}
if dueDate, ok := req.GetArguments()["due_date"].(string); ok && dueDate != "" {
if parsedTime, err := time.Parse(time.RFC3339, dueDate); err == nil {
opt.Deadline = &parsedTime
}
}
if state, ok := req.GetArguments()["state"].(string); ok {
opt.State = gitea_sdk.StateType(state)
}
milestone, _, err := gitea.Client().CreateMilestone(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/%v/milestone err: %v", owner, repo, err))
}
return to.TextResult(milestone)
}
func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetMilestoneFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("id is required"))
}
milestone, _, err := gitea.Client().GetMilestone(owner, repo, int64(id))
if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, int64(id), err))
}
return to.TextResult(milestone)
}
func GetMilestoneByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetMilestoneByNameFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("name is required"))
}
milestone, _, err := gitea.Client().GetMilestoneByName(owner, repo, name)
if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, name, err))
}
return to.TextResult(milestone)
}
func ListRepoMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoMilestonesFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
state, ok := req.GetArguments()["state"].(string)
if !ok {
state = "open"
}
page, ok := req.GetArguments()["page"].(float64)
if !ok {
page = 1
}
limit, ok := req.GetArguments()["limit"].(float64)
if !ok {
limit = 10
}
opt := gitea_sdk.ListMilestoneOption{
State: gitea_sdk.StateType(state),
ListOptions: gitea_sdk.ListOptions{
Page: int(page),
PageSize: int(limit),
},
}
milestones, _, err := gitea.Client().ListRepoMilestones(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/milestones err: %v", owner, repo, err))
}
return to.TextResult(milestones)
}
func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditMilestoneFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("id is required"))
}
opt := gitea_sdk.EditMilestoneOption{}
if title, ok := req.GetArguments()["title"].(string); ok {
opt.Title = title
}
if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = &description
}
if dueDate, ok := req.GetArguments()["due_date"].(string); ok {
if parsedTime, err := time.Parse(time.RFC3339, dueDate); err == nil {
opt.Deadline = &parsedTime
}
}
if state, ok := req.GetArguments()["state"].(string); ok {
stateType := gitea_sdk.StateType(state)
opt.State = &stateType
}
milestone, _, err := gitea.Client().EditMilestone(owner, repo, int64(id), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, int64(id), err))
}
return to.TextResult(milestone)
}
func EditMilestoneByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditMilestoneByNameFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("name is required"))
}
opt := gitea_sdk.EditMilestoneOption{}
if title, ok := req.GetArguments()["title"].(string); ok {
opt.Title = title
}
if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = &description
}
if dueDate, ok := req.GetArguments()["due_date"].(string); ok {
if parsedTime, err := time.Parse(time.RFC3339, dueDate); err == nil {
opt.Deadline = &parsedTime
}
}
if state, ok := req.GetArguments()["state"].(string); ok {
stateType := gitea_sdk.StateType(state)
opt.State = &stateType
}
milestone, _, err := gitea.Client().EditMilestoneByName(owner, repo, name, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, name, err))
}
return to.TextResult(milestone)
}
func DeleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteMilestoneFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("id is required"))
}
_, err := gitea.Client().DeleteMilestone(owner, repo, int64(id))
if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, int64(id), err))
}
return to.TextResult(map[string]interface{}{
"success": true,
"message": fmt.Sprintf("Milestone %d deleted successfully", int64(id)),
})
}
func DeleteMilestoneByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteMilestoneByNameFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("name is required"))
}
_, err := gitea.Client().DeleteMilestoneByName(owner, repo, name)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, name, err))
}
return to.TextResult(map[string]interface{}{
"success": true,
"message": fmt.Sprintf("Milestone '%s' deleted successfully", name),
})
}

View File

@@ -2,10 +2,9 @@ package operation
import (
"fmt"
"net/http"
"gitea.com/gitea/gitea-mcp/operation/issue"
"gitea.com/gitea/gitea-mcp/operation/label"
"gitea.com/gitea/gitea-mcp/operation/milestone"
"gitea.com/gitea/gitea-mcp/operation/pull"
"gitea.com/gitea/gitea-mcp/operation/repo"
"gitea.com/gitea/gitea-mcp/operation/search"
@@ -32,12 +31,6 @@ func RegisterTool(s *server.MCPServer) {
// Pull Tool
s.AddTools(pull.Tool.Tools()...)
// Milestone Tool
s.AddTools(milestone.Tool.Tools()...)
// Label Tool
s.AddTools(label.Tool.Tools()...)
// Search Tool
s.AddTools(search.Tool.Tools()...)
@@ -50,32 +43,56 @@ func RegisterTool(s *server.MCPServer) {
func Run() error {
mcpServer = newMCPServer(flag.Version)
RegisterTool(mcpServer)
addr := fmt.Sprintf("127.0.0.1:%d", flag.Port)
switch flag.Mode {
case "stdio":
if err := server.ServeStdio(
mcpServer,
); err != nil {
if err := server.ServeStdio(mcpServer); err != nil {
return err
}
case "sse":
sseServer := server.NewSSEServer(
mcpServer,
)
log.Infof("Gitea MCP SSE server listening on :%d", flag.Port)
sseServer := server.NewSSEServer(mcpServer)
log.Infof("Gitea MCP Server running:")
log.Infof(" sse: http://%s/", addr)
if err := sseServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil {
return err
}
case "http":
httpServer := server.NewStreamableHTTPServer(
mcpServer,
server.WithLogger(log.New()),
)
log.Infof("Gitea MCP HTTP server listening on :%d", flag.Port)
httpServer := server.NewStreamableHTTPServer(mcpServer)
log.Infof("Gitea MCP Server running:")
log.Infof(" http: http://%s/", addr)
if err := httpServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil {
return err
}
case "network":
// Network mode: serve both HTTP and SSE on same port with different URLs
log.Infof("Network mode: Creating streamable HTTP server...")
streamableServer := server.NewStreamableHTTPServer(mcpServer)
log.Infof("Network mode: Created streamable HTTP server")
log.Infof("Network mode: Creating SSE server...")
sseServer := server.NewSSEServer(mcpServer,
server.WithSSEEndpoint("/sse"),
server.WithMessageEndpoint("/message"),
)
log.Infof("Network mode: Created SSE server")
// Create custom HTTP mux
log.Infof("Network mode: Creating HTTP mux...")
mux := http.NewServeMux()
mux.Handle("/mcp", streamableServer)
mux.Handle("/", sseServer)
log.Infof("Network mode: Configured HTTP routes")
// Start single HTTP server
log.Infof("Gitea MCP Server running in network mode:")
log.Infof(" http: http://%s/mcp", addr)
log.Infof(" sse: http://%s/sse", addr)
err := http.ListenAndServe(fmt.Sprintf(":%d", flag.Port), mux)
log.Errorf("Network mode: ListenAndServe returned with error: %v", err)
return err
default:
return fmt.Errorf("invalid transport type: %s. Must be 'stdio', 'sse' or 'http'", flag.Mode)
return fmt.Errorf("invalid transport type: %s. Must be 'stdio', 'sse', 'http' or 'network'", flag.Mode)
}
return nil
}

View File

@@ -19,55 +19,53 @@ var (
func Default() *zap.Logger {
defaultLoggerOnce.Do(func() {
if defaultLogger != nil {
return
if defaultLogger == nil {
ec := zap.NewProductionEncoderConfig()
ec.EncodeTime = zapcore.TimeEncoderOfLayout(time.DateTime)
ec.EncodeLevel = zapcore.CapitalLevelEncoder
var ws zapcore.WriteSyncer
var wss []zapcore.WriteSyncer
home, _ := os.UserHomeDir()
if home == "" {
home = os.TempDir()
}
logDir := fmt.Sprintf("%s/.gitea-mcp", home)
if err := os.MkdirAll(logDir, 0o700); err != nil {
// Fallback to temp directory if creation fails
logDir = os.TempDir()
}
wss = append(wss, zapcore.AddSync(&lumberjack.Logger{
Filename: fmt.Sprintf("%s/gitea-mcp.log", logDir),
MaxSize: 100,
MaxBackups: 10,
MaxAge: 30,
}))
if flag.Mode == "http" || flag.Mode == "sse" || flag.Mode == "network" {
wss = append(wss, zapcore.AddSync(os.Stdout))
}
ws = zapcore.NewMultiWriteSyncer(wss...)
enc := zapcore.NewConsoleEncoder(ec)
var level zapcore.Level
if flag.Debug {
level = zapcore.DebugLevel
} else {
level = zapcore.InfoLevel
}
core := zapcore.NewCore(enc, ws, level)
options := []zap.Option{
zap.AddStacktrace(zapcore.DPanicLevel),
zap.AddCaller(),
zap.AddCallerSkip(1),
}
defaultLogger = zap.New(core, options...)
}
ec := zap.NewProductionEncoderConfig()
ec.EncodeTime = zapcore.TimeEncoderOfLayout(time.DateTime)
ec.EncodeLevel = zapcore.CapitalLevelEncoder
var ws zapcore.WriteSyncer
var wss []zapcore.WriteSyncer
home, _ := os.UserHomeDir()
if home == "" {
home = os.TempDir()
}
logDir := fmt.Sprintf("%s/.gitea-mcp", home)
if err := os.MkdirAll(logDir, 0o700); err != nil {
// Fallback to temp directory if creation fails
logDir = os.TempDir()
}
wss = append(wss, zapcore.AddSync(&lumberjack.Logger{
Filename: fmt.Sprintf("%s/gitea-mcp.log", logDir),
MaxSize: 100,
MaxBackups: 10,
MaxAge: 30,
}))
if flag.Mode == "http" || flag.Mode == "sse" {
wss = append(wss, zapcore.AddSync(os.Stdout))
}
ws = zapcore.NewMultiWriteSyncer(wss...)
enc := zapcore.NewConsoleEncoder(ec)
var level zapcore.Level
if flag.Debug {
level = zapcore.DebugLevel
} else {
level = zapcore.InfoLevel
}
core := zapcore.NewCore(enc, ws, level)
options := []zap.Option{
zap.AddStacktrace(zapcore.DPanicLevel),
zap.AddCaller(),
zap.AddCallerSkip(1),
}
defaultLogger = zap.New(core, options...)
})
return defaultLogger
@@ -79,22 +77,8 @@ func SetDefault(logger *zap.Logger) {
}
}
func New() *Logger {
return &Logger{
defaultLogger: Default(),
}
}
type Logger struct {
defaultLogger *zap.Logger
}
func (l *Logger) Infof(msg string, args ...any) {
l.defaultLogger.Sugar().Infof(msg, args...)
}
func (l *Logger) Errorf(msg string, args ...any) {
l.defaultLogger.Sugar().Errorf(msg, args...)
func Logger() *zap.Logger {
return defaultLogger
}
func Debug(msg string, fields ...zap.Field) {