cmd.go
1 package cmd 2 3 import ( 4 "context" 5 "flag" 6 "fmt" 7 "net/url" 8 "os" 9 10 "codeberg.org/goern/forgejo-mcp/v2/operation" 11 flagPkg "codeberg.org/goern/forgejo-mcp/v2/pkg/flag" 12 "codeberg.org/goern/forgejo-mcp/v2/pkg/log" 13 ) 14 15 var ( 16 transport string 17 urlFlag string 18 ssePort int 19 httpPort int 20 token string 21 userAgent string 22 23 debug bool 24 ) 25 26 // isVersionRequest returns true for both the "version" subcommand and the 27 // GNU-standard --version / -version flags. All three forms must exit before 28 // flag.Parse() runs so that --url is not required. 29 func isVersionRequest() bool { 30 if len(os.Args) < 2 { 31 return false 32 } 33 arg := os.Args[1] 34 return arg == "version" || arg == "--version" || arg == "-version" 35 } 36 37 // initFlags registers and parses CLI flags using a dedicated FlagSet to avoid 38 // polluting the global flag.CommandLine (which breaks `go test`). 39 func initFlags() { 40 fs := flag.NewFlagSet("forgejo-mcp", flag.ExitOnError) 41 42 fs.StringVar( 43 &transport, 44 "t", 45 "stdio", 46 "Transport type (stdio, sse, or http)", 47 ) 48 fs.StringVar( 49 &transport, 50 "transport", 51 "stdio", 52 "Transport type (stdio, sse, or http)", 53 ) 54 fs.StringVar( 55 &urlFlag, 56 "url", 57 "", 58 "Forgejo instance URL (required, must start with http:// or https://)", 59 ) 60 fs.IntVar( 61 &ssePort, 62 "sse-port", 63 8080, 64 "Port for SSE transport mode", 65 ) 66 fs.IntVar( 67 &httpPort, 68 "http-port", 69 8080, 70 "Port for streamable HTTP transport mode", 71 ) 72 fs.StringVar( 73 &token, 74 "token", 75 "", 76 "Your personal access token", 77 ) 78 fs.StringVar( 79 &userAgent, 80 "user-agent", 81 "", 82 "User agent for HTTP requests (default: forgejo-mcp/<version>)", 83 ) 84 fs.BoolVar( 85 &debug, 86 "d", 87 true, 88 "debug mode", 89 ) 90 fs.BoolVar( 91 &debug, 92 "debug", 93 true, 94 "debug mode", 95 ) 96 97 fs.Parse(os.Args[1:]) 98 99 flagPkg.URL = urlFlag 100 flagPkg.UserAgent = userAgent 101 initConfig() 102 } 103 104 // initConfig resolves URL, token, and debug from flags and environment variables. 105 func initConfig() { 106 if flagPkg.URL == "" { 107 flagPkg.URL = os.Getenv("FORGEJO_URL") 108 if flagPkg.URL != "" { 109 log.Debug("Using FORGEJO_URL environment variable") 110 } 111 } 112 if flagPkg.URL == "" { 113 // Fallback to deprecated GITEA_HOST with warning 114 if giteaHost := os.Getenv("GITEA_HOST"); giteaHost != "" { 115 log.Warn("Deprecated environment variable used", 116 log.StringField("deprecated_var", "GITEA_HOST"), 117 log.StringField("preferred_var", "FORGEJO_URL"), 118 log.StringField("migration_help", "Please update your configuration to use FORGEJO_URL"), 119 ) 120 flagPkg.URL = giteaHost 121 } 122 } 123 if flagPkg.URL == "" { 124 log.Fatal("Missing required configuration", 125 log.StringField("missing", "url"), 126 log.StringField("help", "Provide URL with -url flag or FORGEJO_URL environment variable"), 127 ) 128 } 129 130 // Validate URL has proper scheme 131 log.Debug("Validating URL configuration", 132 log.SanitizedURLField("url", flagPkg.URL), 133 ) 134 if err := validateURL(flagPkg.URL); err != nil { 135 log.Fatal("Invalid URL configuration", 136 log.SanitizedURLField("url", flagPkg.URL), 137 log.ErrorField(err), 138 ) 139 } 140 141 flagPkg.SSEPort = ssePort 142 flagPkg.HTTPPort = httpPort 143 flagPkg.Token = token 144 if flagPkg.Token == "" { 145 flagPkg.Token = os.Getenv("FORGEJO_ACCESS_TOKEN") 146 if flagPkg.Token != "" { 147 log.Debug("Using FORGEJO_ACCESS_TOKEN environment variable") 148 } 149 } 150 if flagPkg.Token == "" { 151 // Fallback to deprecated GITEA_ACCESS_TOKEN with warning 152 if giteaToken := os.Getenv("GITEA_ACCESS_TOKEN"); giteaToken != "" { 153 log.Warn("Deprecated environment variable used", 154 log.StringField("deprecated_var", "GITEA_ACCESS_TOKEN"), 155 log.StringField("preferred_var", "FORGEJO_ACCESS_TOKEN"), 156 log.StringField("migration_help", "Please update your configuration to use FORGEJO_ACCESS_TOKEN"), 157 ) 158 flagPkg.Token = giteaToken 159 } 160 } 161 162 // User agent - CLI flag takes precedence, then environment variable, then default 163 if flagPkg.UserAgent == "" { 164 flagPkg.UserAgent = os.Getenv("FORGEJO_USER_AGENT") 165 if flagPkg.UserAgent != "" { 166 log.Debug("Using FORGEJO_USER_AGENT environment variable") 167 } 168 } 169 170 if debug { 171 flagPkg.Debug = debug 172 log.Debug("Debug mode enabled via flag") 173 } 174 if !debug { 175 flagPkg.Debug = os.Getenv("FORGEJO_DEBUG") == "true" 176 if flagPkg.Debug { 177 log.Debug("Debug mode enabled via FORGEJO_DEBUG environment variable") 178 } 179 if !flagPkg.Debug { 180 // Fallback to deprecated GITEA_DEBUG with warning 181 if os.Getenv("GITEA_DEBUG") == "true" { 182 log.Warn("Deprecated environment variable used", 183 log.StringField("deprecated_var", "GITEA_DEBUG"), 184 log.StringField("preferred_var", "FORGEJO_DEBUG"), 185 log.StringField("migration_help", "Please update your configuration to use FORGEJO_DEBUG"), 186 ) 187 flagPkg.Debug = true 188 } 189 } 190 } 191 } 192 193 func validateURL(urlStr string) error { 194 parsedURL, err := url.Parse(urlStr) 195 if err != nil { 196 return fmt.Errorf("invalid URL format: %w", err) 197 } 198 199 if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { 200 return fmt.Errorf("URL must start with http:// or https://, got: %s", parsedURL.Scheme) 201 } 202 203 if parsedURL.Host == "" { 204 return fmt.Errorf("URL must include a host") 205 } 206 207 return nil 208 } 209 210 func Execute(version string) { 211 if isVersionRequest() { 212 fmt.Printf("forgejo-mcp %s\n", version) 213 return 214 } 215 216 // CLI mode: detect --cli early, skip default flag parsing since CLI 217 // has its own args (tool name, --args, --output) that would confuse it. 218 cliMode = hasCLIFlag() 219 if cliMode { 220 initConfig() 221 } else { 222 initFlags() 223 } 224 225 // Set default user agent if not provided via CLI or env var 226 if flagPkg.UserAgent == "" { 227 flagPkg.UserAgent = "forgejo-mcp/" + version 228 log.Debug("Using default user agent", 229 log.StringField("user_agent", flagPkg.UserAgent), 230 ) 231 } 232 233 defer log.Default().Sync() 234 235 if cliMode { 236 RunCLI(version) 237 return 238 } 239 240 log.Infof("Starting Forgejo MCP Server %s", version) 241 log.Info("Server configuration loaded", 242 log.SanitizedURLField("url", flagPkg.URL), 243 log.StringField("transport", transport), 244 log.IntField("sse-port", flagPkg.SSEPort), 245 log.BoolField("debug", flagPkg.Debug), 246 log.BoolField("token_configured", flagPkg.Token != ""), 247 log.StringField("user_agent", flagPkg.UserAgent), 248 ) 249 250 if err := operation.Run(transport, version); err != nil { 251 if err == context.Canceled { 252 log.Info("Server shutdown due to context cancellation") 253 return 254 } 255 log.Fatalf("Run Forgejo MCP Server Error: %v", err) 256 } 257 }