- Repo-label CRUD: create_repo_label, edit_repo_label, delete_repo_label,
get_repo_label via forgejo-sdk/v3
- Org-label CRUD: create_org_label, edit_org_label, delete_org_label,
get_org_label via pkg/forgejo DoJSON
- Safe delete: delete_mode=safe (default) refuses in-use labels with count;
delete_mode=force overrides; org count best-effort over visible repos
- Color normalization: accepts rrggbb or #rrggbb, 6-digit only, rejects
invalid with error (3-digit shorthand intentionally excluded)
- Resource templates: forgejo://repo/{o}/{r}/label/{id} (single),
forgejo://repo/{o}/{r}/labels (bounded list, EmbeddedListCap ceiling),
forgejo://org/{org}/labels (bounded list); all use operation/resource helpers
- URI parsers: ParseLabel, ParseLabels, ParseOrgLabels added to resource/parse.go
- Unit tests covering happy paths, color normalization, partial PATCH,
empty-edit reject, delete in-use/force/404, resource over-cap truncation
285 lines
7.9 KiB
Go
285 lines
7.9 KiB
Go
package operation
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/actions"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/attachment"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/branchprotection"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/issue"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/org"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/pull"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/release"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/repo"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/search"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/tracking"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/user"
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/version"
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/flag"
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/forgejo"
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/log"
|
|
|
|
"github.com/mark3labs/mcp-go/server"
|
|
)
|
|
|
|
var (
|
|
mcpServer *server.MCPServer
|
|
)
|
|
|
|
func RegisterTool(s *server.MCPServer) {
|
|
log.Info("Registering MCP tools")
|
|
|
|
RegisterUserTool(s)
|
|
RegisterRepoTool(s)
|
|
RegisterIssueTool(s)
|
|
RegisterPullTool(s)
|
|
RegisterPullReviewTool(s)
|
|
RegisterSearchTool(s)
|
|
RegisterVersionTool(s)
|
|
RegisterActionsTool(s)
|
|
RegisterOrgTool(s)
|
|
RegisterTrackingTool(s)
|
|
RegisterAttachmentTool(s)
|
|
RegisterReleaseTool(s)
|
|
RegisterBranchProtectionTool(s)
|
|
|
|
log.Info("All MCP tools registered successfully")
|
|
}
|
|
|
|
// Per-domain registration functions exposed for CLI domain grouping.
|
|
|
|
func RegisterUserTool(s *server.MCPServer) {
|
|
user.RegisterTool(s)
|
|
log.Debug("Registered user tools")
|
|
}
|
|
|
|
func RegisterRepoTool(s *server.MCPServer) {
|
|
repo.RegisterTool(s)
|
|
log.Debug("Registered repository tools")
|
|
}
|
|
|
|
func RegisterIssueTool(s *server.MCPServer) {
|
|
issue.RegisterTool(s)
|
|
log.Debug("Registered issue tools")
|
|
}
|
|
|
|
func RegisterPullTool(s *server.MCPServer) {
|
|
pull.RegisterTool(s)
|
|
log.Debug("Registered pull request tools")
|
|
}
|
|
|
|
func RegisterPullReviewTool(s *server.MCPServer) {
|
|
pull.RegisterReviewTools(s)
|
|
log.Debug("Registered pull review write tools")
|
|
}
|
|
|
|
func RegisterSearchTool(s *server.MCPServer) {
|
|
search.RegisterTool(s)
|
|
log.Debug("Registered search tools")
|
|
}
|
|
|
|
func RegisterVersionTool(s *server.MCPServer) {
|
|
version.RegisterTool(s)
|
|
log.Debug("Registered version tools")
|
|
}
|
|
|
|
func RegisterActionsTool(s *server.MCPServer) {
|
|
actions.RegisterTool(s)
|
|
log.Debug("Registered actions tools")
|
|
}
|
|
|
|
func RegisterOrgTool(s *server.MCPServer) {
|
|
org.RegisterTool(s)
|
|
log.Debug("Registered org tools")
|
|
}
|
|
|
|
func RegisterTrackingTool(s *server.MCPServer) {
|
|
tracking.RegisterTool(s)
|
|
log.Debug("Registered time tracking tools")
|
|
}
|
|
|
|
func RegisterAttachmentTool(s *server.MCPServer) {
|
|
attachment.RegisterTool(s)
|
|
log.Debug("Registered attachment tools")
|
|
}
|
|
|
|
func RegisterReleaseTool(s *server.MCPServer) {
|
|
release.RegisterTool(s)
|
|
log.Debug("Registered release tools")
|
|
}
|
|
|
|
func RegisterBranchProtectionTool(s *server.MCPServer) {
|
|
branchprotection.RegisterTool(s)
|
|
log.Debug("Registered branch protection tools")
|
|
}
|
|
|
|
func extractToken(auth string) string {
|
|
if auth == "" {
|
|
return ""
|
|
}
|
|
parts := strings.SplitN(auth, " ", 2)
|
|
if len(parts) != 2 {
|
|
// No scheme prefix. Spec: bare tokens MUST be rejected and treated
|
|
// as if no Authorization header were present (no silent identity).
|
|
return ""
|
|
}
|
|
scheme := strings.ToLower(parts[0])
|
|
if scheme == "token" || scheme == "bearer" {
|
|
return parts[1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func Run(transport, version string) error {
|
|
flag.Version = version
|
|
mcpServer = newMCPServer(version)
|
|
RegisterTool(mcpServer)
|
|
RegisterCoreResources(mcpServer)
|
|
|
|
// Test connection to Forgejo instance before starting the server
|
|
log.Info("Testing connection to Forgejo instance",
|
|
log.SanitizedURLField("url", flag.URL),
|
|
)
|
|
if err := testConnection(); err != nil {
|
|
log.Error("Failed to connect to Forgejo instance",
|
|
log.SanitizedURLField("url", flag.URL),
|
|
log.ErrorField(err),
|
|
)
|
|
return fmt.Errorf("connection test failed: %w", err)
|
|
}
|
|
log.Info("Successfully connected to Forgejo instance",
|
|
log.SanitizedURLField("url", flag.URL),
|
|
)
|
|
|
|
switch transport {
|
|
case "stdio":
|
|
log.Info("Starting MCP server with stdio transport")
|
|
log.Info("MCP server ready for stdio communication")
|
|
if err := server.ServeStdio(mcpServer); err != nil {
|
|
log.Error("MCP stdio server failed",
|
|
log.ErrorField(err),
|
|
)
|
|
return err
|
|
}
|
|
log.Info("MCP stdio server shutdown")
|
|
case "sse":
|
|
sseServer := server.NewSSEServer(mcpServer, server.WithSSEContextFunc(func(ctx context.Context, r *http.Request) context.Context {
|
|
if token := extractToken(r.Header.Get("Authorization")); token != "" {
|
|
return forgejo.WithToken(ctx, token)
|
|
}
|
|
return ctx
|
|
}))
|
|
log.Info("Starting MCP SSE server",
|
|
log.IntField("port", flag.SSEPort),
|
|
)
|
|
log.Info("MCP SSE server ready for connections",
|
|
log.IntField("port", flag.SSEPort),
|
|
log.StringField("endpoint", fmt.Sprintf("http://localhost:%d", flag.SSEPort)),
|
|
)
|
|
if err := sseServer.Start(fmt.Sprintf(":%d", flag.SSEPort)); err != nil {
|
|
log.Error("Failed to start SSE server",
|
|
log.IntField("port", flag.SSEPort),
|
|
log.ErrorField(err),
|
|
)
|
|
return fmt.Errorf("failed to start SSE server: %w", err)
|
|
}
|
|
log.Info("MCP SSE server shutdown")
|
|
case "http":
|
|
httpServer := server.NewStreamableHTTPServer(mcpServer, server.WithHTTPContextFunc(func(ctx context.Context, r *http.Request) context.Context {
|
|
if token := extractToken(r.Header.Get("Authorization")); token != "" {
|
|
return forgejo.WithToken(ctx, token)
|
|
}
|
|
return ctx
|
|
}))
|
|
log.Info("Starting MCP streamable HTTP server",
|
|
log.IntField("port", flag.HTTPPort),
|
|
)
|
|
log.Info("MCP streamable HTTP server ready for connections",
|
|
log.IntField("port", flag.HTTPPort),
|
|
log.StringField("endpoint", fmt.Sprintf("http://localhost:%d", flag.HTTPPort)),
|
|
)
|
|
if err := httpServer.Start(fmt.Sprintf(":%d", flag.HTTPPort)); err != nil {
|
|
log.Error("Failed to start streamable HTTP server",
|
|
log.IntField("port", flag.HTTPPort),
|
|
log.ErrorField(err),
|
|
)
|
|
return fmt.Errorf("failed to start streamable HTTP server: %w", err)
|
|
}
|
|
log.Info("MCP streamable HTTP server shutdown")
|
|
default:
|
|
log.Error("Invalid transport configuration",
|
|
log.StringField("transport", transport),
|
|
log.StringField("valid_options", "stdio, sse, http"),
|
|
)
|
|
return fmt.Errorf("invalid transport type: %s. Must be 'stdio', 'sse', or 'http'", transport)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func testConnection() error {
|
|
return forgejo.VerifyConnection()
|
|
}
|
|
|
|
func RegisterCoreResources(s *server.MCPServer) {
|
|
RegisterCommitResource(s)
|
|
RegisterIssueResources(s)
|
|
RegisterLabelResources(s)
|
|
RegisterOwnerResource(s)
|
|
RegisterPullResources(s)
|
|
RegisterRepoResource(s)
|
|
RegisterStatusResource(s)
|
|
RegisterBranchProtectionResources(s)
|
|
log.Debug("Registered core resource templates")
|
|
}
|
|
|
|
func RegisterLabelResources(s *server.MCPServer) {
|
|
issue.RegisterLabelResources(s)
|
|
log.Debug("Registered label resource templates")
|
|
}
|
|
|
|
func RegisterBranchProtectionResources(s *server.MCPServer) {
|
|
branchprotection.RegisterResource(s)
|
|
log.Debug("Registered branch protection resource templates")
|
|
}
|
|
|
|
func RegisterPullResources(s *server.MCPServer) {
|
|
pull.RegisterPullResources(s)
|
|
log.Debug("Registered pull request resource template")
|
|
}
|
|
|
|
func RegisterIssueResources(s *server.MCPServer) {
|
|
issue.RegisterIssueResources(s)
|
|
log.Debug("Registered issue resource templates")
|
|
}
|
|
|
|
func RegisterCommitResource(s *server.MCPServer) {
|
|
repo.RegisterCommitResource(s)
|
|
log.Debug("Registered commit resource template")
|
|
}
|
|
|
|
func RegisterOwnerResource(s *server.MCPServer) {
|
|
user.RegisterOwnerResource(s)
|
|
log.Debug("Registered owner resource template")
|
|
}
|
|
|
|
func RegisterRepoResource(s *server.MCPServer) {
|
|
repo.RegisterRepoResource(s)
|
|
log.Debug("Registered repo resource template")
|
|
}
|
|
|
|
func RegisterStatusResource(s *server.MCPServer) {
|
|
repo.RegisterStatusResource(s)
|
|
log.Debug("Registered commit status resource template")
|
|
}
|
|
|
|
func newMCPServer(version string) *server.MCPServer {
|
|
return server.NewMCPServer(
|
|
"Forgejo MCP Server",
|
|
version,
|
|
server.WithLogging(),
|
|
server.WithResourceCapabilities(false, false),
|
|
)
|
|
}
|