package cmd import ( "context" "fmt" "net/http" "os" "strings" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) var ( cfgFile string serverFlag string tokenFlag string outputFlag string verbose bool cfg Config ) const cliUserAgent = "amcs-cli/0.0.1" var rootCmd = &cobra.Command{ Use: "amcs-cli", Short: "CLI for connecting to a remote AMCS MCP server", SilenceUsage: true, SilenceErrors: true, PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { return loadConfig() }, } func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Path to config file") rootCmd.PersistentFlags().StringVar(&serverFlag, "server", "", "AMCS server URL") rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "AMCS bearer token") rootCmd.PersistentFlags().StringVar(&outputFlag, "output", "json", "Output format: json or yaml") rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Enable verbose logging to stderr") } func loadConfig() error { path, err := resolveConfigPath(cfgFile) if err != nil { return err } loaded, err := loadConfigFile(path) if err != nil { return err } cfg = loaded if v := strings.TrimSpace(os.Getenv("AMCS_SERVER")); v != "" { cfg.Server = v } if v := strings.TrimSpace(os.Getenv("AMCS_URL")); v != "" { cfg.Server = v } if v := strings.TrimSpace(os.Getenv("AMCS_TOKEN")); v != "" { cfg.Token = v } if v := strings.TrimSpace(serverFlag); v != "" { cfg.Server = v } if v := strings.TrimSpace(tokenFlag); v != "" { cfg.Token = v } outputFlag = strings.ToLower(strings.TrimSpace(outputFlag)) if outputFlag != "json" && outputFlag != "yaml" { return fmt.Errorf("invalid --output %q: must be json or yaml", outputFlag) } return nil } func requireServer() error { if strings.TrimSpace(cfg.Server) == "" { return fmt.Errorf("server URL is required; set --server, AMCS_SERVER, AMCS_URL, or config server") } return nil } func endpointURL() string { base := strings.TrimRight(strings.TrimSpace(cfg.Server), "/") if strings.HasSuffix(base, "/mcp") { return base } return base + "/mcp" } func newHTTPClient() *http.Client { return &http.Client{ Timeout: 0, Transport: &bearerTransport{ base: http.DefaultTransport, token: cfg.Token, }, } } type bearerTransport struct { base http.RoundTripper token string } func (t *bearerTransport) RoundTrip(req *http.Request) (*http.Response, error) { base := t.base if base == nil { base = http.DefaultTransport } clone := req.Clone(req.Context()) if strings.TrimSpace(clone.Header.Get("User-Agent")) == "" { clone.Header.Set("User-Agent", cliUserAgent) } if strings.TrimSpace(t.token) != "" { clone.Header.Set("Authorization", "Bearer "+t.token) } return base.RoundTrip(clone) } func connectRemote(ctx context.Context) (*mcp.ClientSession, error) { if err := requireServer(); err != nil { return nil, err } verboseLogf("connecting to %s", endpointURL()) client := mcp.NewClient(&mcp.Implementation{Name: "amcs-cli", Version: "0.0.1"}, nil) transport := &mcp.StreamableClientTransport{ Endpoint: endpointURL(), HTTPClient: newHTTPClient(), DisableStandaloneSSE: true, } session, err := client.Connect(ctx, transport, nil) if err != nil { return nil, fmt.Errorf("connect to AMCS server: %w", err) } verboseLogf("connected to %s", endpointURL()) return session, nil } func verboseLogf(format string, args ...any) { if !verbose { return } _, _ = fmt.Fprintf(os.Stderr, "[amcs-cli] "+format+"\n", args...) }