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), ) }