/ cmd / cmd.go
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  }