# CLI Management Commands Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add `mcpproxy upstream` and `mcpproxy doctor` commands for managing upstream servers and monitoring system health via CLI.

**Architecture:** Two new command groups following existing CLI patterns. Commands auto-detect daemon via socket and support both daemon mode (fast API calls) and standalone mode (direct connections). Uses existing `internal/cliclient` for API communication and adds new endpoints to `internal/httpapi` for diagnostics.

**Tech Stack:** Go, Cobra CLI, internal/cliclient, internal/httpapi, golang.org/x/term (for TTY detection)

---

## Task 1: Add confirmation helper for bulk operations

**Files:**
- Create: `cmd/mcpproxy/confirmation.go`
- Test: Manual testing (no unit tests for TTY interaction)

**Step 1: Create confirmation helper file**

Create `cmd/mcpproxy/confirmation.go`:

```go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"

	"golang.org/x/term"
)

// confirmBulkAction prompts user for confirmation when performing bulk operations.
// Returns (true, nil) if user confirms or force=true
// Returns (false, nil) if user declines
// Returns (false, error) if non-interactive without force flag
func confirmBulkAction(action string, count int, force bool) (bool, error) {
	// Skip prompt if force flag provided
	if force {
		return true, nil
	}

	// Check if stdin is a TTY (interactive terminal)
	if !term.IsTerminal(int(os.Stdin.Fd())) {
		return false, fmt.Errorf("--all requires --force flag in non-interactive mode")
	}

	// Show confirmation prompt
	fmt.Printf("⚠️  This will %s %d server(s). Continue? [y/N]: ", action, count)

	// Read user input
	reader := bufio.NewReader(os.Stdin)
	response, err := reader.ReadString('\n')
	if err != nil {
		return false, fmt.Errorf("failed to read confirmation: %w", err)
	}

	// Parse response (accept y, yes case-insensitive)
	response = strings.ToLower(strings.TrimSpace(response))
	return response == "y" || response == "yes", nil
}
```

**Step 2: Commit**

```bash
git add cmd/mcpproxy/confirmation.go
git commit -m "feat: add confirmation helper for bulk operations"
```

---

## Task 2: Add `mcpproxy upstream list` command

**Files:**
- Create: `cmd/mcpproxy/upstream_cmd.go`
- Modify: `cmd/mcpproxy/main.go`
- Modify: `internal/cliclient/client.go`

**Step 1: Create upstream command structure**

Create `cmd/mcpproxy/upstream_cmd.go`:

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"time"

	"github.com/spf13/cobra"
	"go.uber.org/zap"

	"github.com/smart-mcp-proxy/mcpproxy-go/internal/cliclient"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/socket"
)

var (
	upstreamCmd = &cobra.Command{
		Use:   "upstream",
		Short: "Manage upstream MCP servers",
		Long:  "Commands for managing and monitoring upstream MCP servers",
	}

	upstreamListCmd = &cobra.Command{
		Use:   "list",
		Short: "List all upstream servers with status",
		Long: `List all configured upstream MCP servers with connection status, tool counts, and errors.

Examples:
  mcpproxy upstream list
  mcpproxy upstream list --output=json
  mcpproxy upstream list --log-level=debug`,
		RunE: runUpstreamList,
	}

	// Command flags
	upstreamOutputFormat string
	upstreamLogLevel     string
	upstreamConfigPath   string
)

// GetUpstreamCommand returns the upstream command for adding to the root command
func GetUpstreamCommand() *cobra.Command {
	return upstreamCmd
}

func init() {
	// Add subcommands
	upstreamCmd.AddCommand(upstreamListCmd)

	// Define flags
	upstreamListCmd.Flags().StringVarP(&upstreamOutputFormat, "output", "o", "table", "Output format (table, json)")
	upstreamListCmd.Flags().StringVarP(&upstreamLogLevel, "log-level", "l", "warn", "Log level (trace, debug, info, warn, error)")
	upstreamListCmd.Flags().StringVarP(&upstreamConfigPath, "config", "c", "", "Path to MCP configuration file")
}

func runUpstreamList(_ *cobra.Command, _ []string) error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Load configuration
	globalConfig, err := loadUpstreamConfig()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error loading configuration: %v\n", err)
		return err
	}

	// Create logger
	logger, err := createUpstreamLogger(upstreamLogLevel)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error creating logger: %v\n", err)
		return err
	}

	// Check if daemon is running
	if shouldUseUpstreamDaemon(globalConfig.DataDir) {
		logger.Info("Detected running daemon, using client mode via socket")
		return runUpstreamListClientMode(ctx, globalConfig.DataDir, logger)
	}

	// No daemon - load from config file
	logger.Info("No daemon detected, reading from config file")
	return runUpstreamListFromConfig(globalConfig)
}

func shouldUseUpstreamDaemon(dataDir string) bool {
	socketPath := socket.DetectSocketPath(dataDir)
	return socket.IsSocketAvailable(socketPath)
}

func runUpstreamListClientMode(ctx context.Context, dataDir string, logger *zap.Logger) error {
	socketPath := socket.DetectSocketPath(dataDir)
	client := cliclient.NewClient(socketPath, logger.Sugar())

	// Call GET /api/v1/servers
	servers, err := client.GetServers(ctx)
	if err != nil {
		return fmt.Errorf("failed to get servers from daemon: %w", err)
	}

	return outputServers(servers)
}

func runUpstreamListFromConfig(globalConfig *config.Config) error {
	// Convert config servers to output format
	servers := make([]map[string]interface{}, len(globalConfig.Servers))
	for i, srv := range globalConfig.Servers {
		servers[i] = map[string]interface{}{
			"name":       srv.Name,
			"enabled":    srv.Enabled,
			"protocol":   srv.Protocol,
			"connected":  false,
			"tool_count": 0,
			"status":     "unknown (daemon not running)",
		}
	}

	return outputServers(servers)
}

func outputServers(servers []map[string]interface{}) error {
	switch upstreamOutputFormat {
	case "json":
		output, err := json.MarshalIndent(servers, "", "  ")
		if err != nil {
			return fmt.Errorf("failed to format output: %w", err)
		}
		fmt.Println(string(output))
	case "table":
	default:
		// Table format
		fmt.Printf("%-25s %-10s %-10s %-12s %-10s %s\n",
			"NAME", "ENABLED", "PROTOCOL", "CONNECTED", "TOOLS", "STATUS")
		fmt.Printf("%-25s %-10s %-10s %-12s %-10s %s\n",
			"====", "=======", "========", "=========", "=====", "======")

		for _, srv := range servers {
			name := getStringField(srv, "name")
			enabled := getBoolField(srv, "enabled")
			protocol := getStringField(srv, "protocol")
			connected := getBoolField(srv, "connected")
			toolCount := getIntField(srv, "tool_count")
			status := getStringField(srv, "status")

			enabledStr := "no"
			if enabled {
				enabledStr = "yes"
			}

			connectedStr := "no"
			if connected {
				connectedStr = "yes"
			}

			fmt.Printf("%-25s %-10s %-10s %-12s %-10d %s\n",
				name, enabledStr, protocol, connectedStr, toolCount, status)
		}
	}

	return nil
}

func loadUpstreamConfig() (*config.Config, error) {
	if upstreamConfigPath != "" {
		return config.LoadFromFile(upstreamConfigPath)
	}
	return config.Load()
}

func createUpstreamLogger(level string) (*zap.Logger, error) {
	var zapLevel zap.AtomicLevel
	switch level {
	case "trace", "debug":
		zapLevel = zap.NewAtomicLevelAt(zap.DebugLevel)
	case "info":
		zapLevel = zap.NewAtomicLevelAt(zap.InfoLevel)
	case "warn":
		zapLevel = zap.NewAtomicLevelAt(zap.WarnLevel)
	case "error":
		zapLevel = zap.NewAtomicLevelAt(zap.ErrorLevel)
	default:
		zapLevel = zap.NewAtomicLevelAt(zap.WarnLevel)
	}

	cfg := zap.Config{
		Level:            zapLevel,
		Development:      false,
		Encoding:         "console",
		EncoderConfig:    zap.NewDevelopmentEncoderConfig(),
		OutputPaths:      []string{"stderr"},
		ErrorOutputPaths: []string{"stderr"},
	}

	return cfg.Build()
}

func getStringField(m map[string]interface{}, key string) string {
	if v, ok := m[key].(string); ok {
		return v
	}
	return ""
}

func getBoolField(m map[string]interface{}, key string) bool {
	if v, ok := m[key].(bool); ok {
		return v
	}
	return false
}

func getIntField(m map[string]interface{}, key string) int {
	if v, ok := m[key].(float64); ok {
		return int(v)
	}
	if v, ok := m[key].(int); ok {
		return v
	}
	return 0
}
```

**Step 2: Add GetServers method to cliclient**

Modify `internal/cliclient/client.go`, add this method:

```go
// GetServers retrieves list of servers from daemon.
func (c *Client) GetServers(ctx context.Context) ([]map[string]interface{}, error) {
	url := c.baseURL + "/api/v1/servers"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to call servers API: %w", err)
	}
	defer resp.Body.Close()

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var apiResp struct {
		Success bool `json:"success"`
		Data    struct {
			Servers []map[string]interface{} `json:"servers"`
		} `json:"data"`
		Error string `json:"error"`
	}

	if err := json.Unmarshal(bodyBytes, &apiResp); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if !apiResp.Success {
		return nil, fmt.Errorf("API call failed: %s", apiResp.Error)
	}

	return apiResp.Data.Servers, nil
}
```

**Step 3: Register upstream command in main.go**

Modify `cmd/mcpproxy/main.go`, add after other command registrations (around line 113-126):

```go
// Add after secretsCmd := GetSecretsCommand()
upstreamCmd := GetUpstreamCommand()

// Add after rootCmd.AddCommand(secretsCmd)
rootCmd.AddCommand(upstreamCmd)
```

**Step 4: Test manually**

```bash
# Build
go build -o mcpproxy ./cmd/mcpproxy

# Test with daemon not running
./mcpproxy upstream list

# Expected: Table showing servers from config with "unknown (daemon not running)" status
```

**Step 5: Commit**

```bash
git add cmd/mcpproxy/upstream_cmd.go cmd/mcpproxy/main.go internal/cliclient/client.go
git commit -m "feat: add 'mcpproxy upstream list' command"
```

---

## Task 3: Add `mcpproxy upstream logs` command

**Files:**
- Modify: `cmd/mcpproxy/upstream_cmd.go`
- Modify: `internal/cliclient/client.go`

**Step 1: Add logs subcommand to upstream_cmd.go**

Add to `cmd/mcpproxy/upstream_cmd.go` in the `var` block:

```go
	upstreamLogsCmd = &cobra.Command{
		Use:   "logs <server-name>",
		Short: "Show logs for a specific server",
		Long: `Display recent log entries from a specific upstream server.

Examples:
  mcpproxy upstream logs github-server
  mcpproxy upstream logs github-server --tail=100
  mcpproxy upstream logs weather-api --follow`,
		Args: cobra.ExactArgs(1),
		RunE: runUpstreamLogs,
	}

	// Add new flags
	upstreamLogsTail   int
	upstreamLogsFollow bool
```

Add to `init()` function:

```go
	upstreamCmd.AddCommand(upstreamLogsCmd)

	upstreamLogsCmd.Flags().IntVarP(&upstreamLogsTail, "tail", "n", 50, "Number of log lines to show")
	upstreamLogsCmd.Flags().BoolVarP(&upstreamLogsFollow, "follow", "f", false, "Follow log output (requires daemon)")
	upstreamLogsCmd.Flags().StringVarP(&upstreamLogLevel, "log-level", "l", "warn", "Log level")
	upstreamLogsCmd.Flags().StringVarP(&upstreamConfigPath, "config", "c", "", "Path to config file")
```

Add implementation functions:

```go
import (
	"os/exec"
	"path/filepath"
)

func runUpstreamLogs(cmd *cobra.Command, args []string) error {
	serverName := args[0]

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Load configuration
	globalConfig, err := loadUpstreamConfig()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error loading configuration: %v\n", err)
		return err
	}

	// Create logger
	logger, err := createUpstreamLogger(upstreamLogLevel)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error creating logger: %v\n", err)
		return err
	}

	// Follow mode requires daemon
	if upstreamLogsFollow {
		if !shouldUseUpstreamDaemon(globalConfig.DataDir) {
			return fmt.Errorf("--follow requires running daemon")
		}
		logger.Info("Following logs from daemon")
		return runUpstreamLogsFollowMode(ctx, globalConfig.DataDir, serverName, logger)
	}

	// Check if daemon is running
	if shouldUseUpstreamDaemon(globalConfig.DataDir) {
		logger.Info("Detected running daemon, using client mode via socket")
		return runUpstreamLogsClientMode(ctx, globalConfig.DataDir, serverName, logger)
	}

	// No daemon - read from log file
	logger.Info("No daemon detected, reading from log file")
	return runUpstreamLogsFromFile(globalConfig, serverName)
}

func runUpstreamLogsClientMode(ctx context.Context, dataDir, serverName string, logger *zap.Logger) error {
	socketPath := socket.DetectSocketPath(dataDir)
	client := cliclient.NewClient(socketPath, logger.Sugar())

	// Call GET /api/v1/servers/{name}/logs?tail=N
	logs, err := client.GetServerLogs(ctx, serverName, upstreamLogsTail)
	if err != nil {
		return fmt.Errorf("failed to get logs from daemon: %w", err)
	}

	for _, logLine := range logs {
		fmt.Println(logLine)
	}

	return nil
}

func runUpstreamLogsFromFile(globalConfig *config.Config, serverName string) error {
	// Read from log file directly
	logDir := globalConfig.Logging.LogDir
	if logDir == "" {
		// Use default log directory
		homeDir, _ := os.UserHomeDir()
		logDir = filepath.Join(homeDir, ".mcpproxy", "logs")
	}

	logFile := filepath.Join(logDir, fmt.Sprintf("server-%s.log", serverName))

	// Check if file exists
	if _, err := os.Stat(logFile); os.IsNotExist(err) {
		return fmt.Errorf("log file not found: %s (daemon may not have run yet)", logFile)
	}

	// Read last N lines using tail command
	cmd := exec.Command("tail", "-n", fmt.Sprintf("%d", upstreamLogsTail), logFile)
	output, err := cmd.Output()
	if err != nil {
		return fmt.Errorf("failed to read log file: %w", err)
	}

	fmt.Print(string(output))
	return nil
}

func runUpstreamLogsFollowMode(ctx context.Context, dataDir, serverName string, logger *zap.Logger) error {
	socketPath := socket.DetectSocketPath(dataDir)
	client := cliclient.NewClient(socketPath, logger.Sugar())

	fmt.Printf("Following logs for server '%s' (Ctrl+C to stop)...\n", serverName)

	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	lastLines := make(map[string]bool)

	for {
		select {
		case <-ctx.Done():
			return nil
		case <-ticker.C:
			logs, err := client.GetServerLogs(ctx, serverName, 10)
			if err != nil {
				logger.Warn("Failed to fetch logs", zap.Error(err))
				continue
			}

			// Print only new lines
			for _, line := range logs {
				if !lastLines[line] {
					fmt.Println(line)
					lastLines[line] = true
				}
			}
		}
	}
}
```

**Step 2: Add GetServerLogs method to cliclient**

Add to `internal/cliclient/client.go`:

```go
// GetServerLogs retrieves logs for a specific server.
func (c *Client) GetServerLogs(ctx context.Context, serverName string, tail int) ([]string, error) {
	url := fmt.Sprintf("%s/api/v1/servers/%s/logs?tail=%d", c.baseURL, serverName, tail)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to call logs API: %w", err)
	}
	defer resp.Body.Close()

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var apiResp struct {
		Success bool `json:"success"`
		Data    struct {
			Logs []string `json:"logs"`
		} `json:"data"`
		Error string `json:"error"`
	}

	if err := json.Unmarshal(bodyBytes, &apiResp); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if !apiResp.Success {
		return nil, fmt.Errorf("API call failed: %s", apiResp.Error)
	}

	return apiResp.Data.Logs, nil
}
```

**Step 3: Test manually**

```bash
go build -o mcpproxy ./cmd/mcpproxy

# Test without daemon (should read from file or error)
./mcpproxy upstream logs github-server
```

**Step 4: Commit**

```bash
git add cmd/mcpproxy/upstream_cmd.go internal/cliclient/client.go
git commit -m "feat: add 'mcpproxy upstream logs' command with follow mode"
```

---

## Task 4: Add `mcpproxy upstream enable/disable/restart` commands

**Files:**
- Modify: `cmd/mcpproxy/upstream_cmd.go`
- Modify: `internal/cliclient/client.go`

**Step 1: Add enable/disable/restart subcommands**

Add to `cmd/mcpproxy/upstream_cmd.go` in the `var` block:

```go
	upstreamEnableCmd = &cobra.Command{
		Use:   "enable <server-name>",
		Short: "Enable a server",
		Args:  cobra.MaximumNArgs(1),
		RunE:  runUpstreamEnable,
	}

	upstreamDisableCmd = &cobra.Command{
		Use:   "disable <server-name>",
		Short: "Disable a server",
		Args:  cobra.MaximumNArgs(1),
		RunE:  runUpstreamDisable,
	}

	upstreamRestartCmd = &cobra.Command{
		Use:   "restart <server-name>",
		Short: "Restart a server",
		Args:  cobra.MaximumNArgs(1),
		RunE:  runUpstreamRestart,
	}

	// Flags for bulk operations
	upstreamAll   bool
	upstreamForce bool
```

Add to `init()` function:

```go
	upstreamCmd.AddCommand(upstreamEnableCmd)
	upstreamCmd.AddCommand(upstreamDisableCmd)
	upstreamCmd.AddCommand(upstreamRestartCmd)

	// Add --all and --force flags to enable/disable/restart
	upstreamEnableCmd.Flags().BoolVar(&upstreamAll, "all", false, "Enable all servers")
	upstreamEnableCmd.Flags().BoolVar(&upstreamForce, "force", false, "Skip confirmation prompt")

	upstreamDisableCmd.Flags().BoolVar(&upstreamAll, "all", false, "Disable all servers")
	upstreamDisableCmd.Flags().BoolVar(&upstreamForce, "force", false, "Skip confirmation prompt")

	upstreamRestartCmd.Flags().BoolVar(&upstreamAll, "all", false, "Restart all servers")
```

Add implementation functions:

```go
func runUpstreamEnable(cmd *cobra.Command, args []string) error {
	if upstreamAll {
		return runUpstreamBulkAction("enable", upstreamForce)
	}
	if len(args) == 0 {
		return fmt.Errorf("server name required (or use --all)")
	}
	return runUpstreamAction(args[0], "enable")
}

func runUpstreamDisable(cmd *cobra.Command, args []string) error {
	if upstreamAll {
		return runUpstreamBulkAction("disable", upstreamForce)
	}
	if len(args) == 0 {
		return fmt.Errorf("server name required (or use --all)")
	}
	return runUpstreamAction(args[0], "disable")
}

func runUpstreamRestart(cmd *cobra.Command, args []string) error {
	if upstreamAll {
		return runUpstreamBulkAction("restart", false) // restart doesn't need confirmation
	}
	if len(args) == 0 {
		return fmt.Errorf("server name required (or use --all)")
	}
	return runUpstreamAction(args[0], "restart")
}

func runUpstreamAction(serverName, action string) error {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Load configuration
	globalConfig, err := loadUpstreamConfig()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error loading configuration: %v\n", err)
		return err
	}

	// Create logger
	logger, err := createUpstreamLogger(upstreamLogLevel)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error creating logger: %v\n", err)
		return err
	}

	// Require daemon for actions
	if !shouldUseUpstreamDaemon(globalConfig.DataDir) {
		return fmt.Errorf("server actions require running daemon. Start with: mcpproxy serve")
	}

	socketPath := socket.DetectSocketPath(globalConfig.DataDir)
	client := cliclient.NewClient(socketPath, logger.Sugar())

	fmt.Printf("Performing action '%s' on server '%s'...\n", action, serverName)

	err = client.ServerAction(ctx, serverName, action)
	if err != nil {
		return fmt.Errorf("failed to %s server: %w", action, err)
	}

	fmt.Printf("✅ Successfully %sd server '%s'\n", action, serverName)
	return nil
}

func runUpstreamBulkAction(action string, force bool) error {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Load configuration
	globalConfig, err := loadUpstreamConfig()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error loading configuration: %v\n", err)
		return err
	}

	// Create logger
	logger, err := createUpstreamLogger(upstreamLogLevel)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error creating logger: %v\n", err)
		return err
	}

	// Require daemon
	if !shouldUseUpstreamDaemon(globalConfig.DataDir) {
		return fmt.Errorf("server actions require running daemon. Start with: mcpproxy serve")
	}

	socketPath := socket.DetectSocketPath(globalConfig.DataDir)
	client := cliclient.NewClient(socketPath, logger.Sugar())

	// Get server list to count
	servers, err := client.GetServers(ctx)
	if err != nil {
		return fmt.Errorf("failed to get server list: %w", err)
	}

	// Filter based on action (enable=disabled servers, disable=enabled servers)
	var targetServers []string
	for _, srv := range servers {
		name := getStringField(srv, "name")
		enabled := getBoolField(srv, "enabled")

		if action == "enable" && !enabled {
			targetServers = append(targetServers, name)
		} else if action == "disable" && enabled {
			targetServers = append(targetServers, name)
		} else if action == "restart" && enabled {
			targetServers = append(targetServers, name)
		}
	}

	if len(targetServers) == 0 {
		fmt.Printf("⚠️  No servers to %s\n", action)
		return nil
	}

	// Require confirmation for enable/disable --all
	if action == "enable" || action == "disable" {
		confirmed, err := confirmBulkAction(action, len(targetServers), force)
		if err != nil {
			return err
		}
		if !confirmed {
			fmt.Println("Operation cancelled")
			return nil
		}
	}

	// Perform action on all servers
	fmt.Printf("Performing action '%s' on %d server(s)...\n", action, len(targetServers))

	for _, serverName := range targetServers {
		err = client.ServerAction(ctx, serverName, action)
		if err != nil {
			fmt.Fprintf(os.Stderr, "❌ Failed to %s server '%s': %v\n", action, serverName, err)
		} else {
			fmt.Printf("✅ Successfully %sd server '%s'\n", action, serverName)
		}
	}

	return nil
}
```

**Step 2: Add ServerAction method to cliclient**

Add to `internal/cliclient/client.go`:

```go
// ServerAction performs an action on a server (enable, disable, restart).
func (c *Client) ServerAction(ctx context.Context, serverName, action string) error {
	var url string
	method := http.MethodPost

	switch action {
	case "enable":
		url = fmt.Sprintf("%s/api/v1/servers/%s/enable", c.baseURL, serverName)
	case "disable":
		url = fmt.Sprintf("%s/api/v1/servers/%s/disable", c.baseURL, serverName)
	case "restart":
		url = fmt.Sprintf("%s/api/v1/servers/%s/restart", c.baseURL, serverName)
	default:
		return fmt.Errorf("unknown action: %s", action)
	}

	req, err := http.NewRequestWithContext(ctx, method, url, nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to call server action API: %w", err)
	}
	defer resp.Body.Close()

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var apiResp struct {
		Success bool   `json:"success"`
		Error   string `json:"error"`
	}

	if err := json.Unmarshal(bodyBytes, &apiResp); err != nil {
		return fmt.Errorf("failed to parse response: %w", err)
	}

	if !apiResp.Success {
		return fmt.Errorf("action failed: %s", apiResp.Error)
	}

	return nil
}
```

**Step 3: Test manually**

```bash
go build -o mcpproxy ./cmd/mcpproxy

# Test single server (should error without daemon)
./mcpproxy upstream enable github-server

# Expected: Error message about requiring daemon
```

**Step 4: Commit**

```bash
git add cmd/mcpproxy/upstream_cmd.go internal/cliclient/client.go
git commit -m "feat: add upstream enable/disable/restart commands with --all support"
```

---

## Task 5: Add `mcpproxy doctor` command

**Status:** ✅ JSON output complete, ⚠️ Pretty output needs implementation

**Files:**
- Create: `cmd/mcpproxy/doctor_cmd.go`
- Modify: `cmd/mcpproxy/main.go`

**Implementation Notes:**
- ✅ JSON output (`--output=json`) is fully functional and displays real diagnostics data
- ⚠️ Pretty output (default) is currently a placeholder showing generic "all systems operational" message
- TODO: Implement pretty output formatting to parse and display:
  - `upstream_errors` - Array of server connection errors with server names and error messages
  - `oauth_required` - Servers requiring OAuth authentication
  - `missing_secrets` - Unresolved secret references
  - `runtime_warnings` - General runtime warnings
  - `total_issues` - Total count of all issues
- Reference: See `docs/plans/2025-11-19-cli-management-commands-design.md` lines 700-764 for intended pretty output format with categorized issues and remediation steps

**Step 1: Create doctor command structure**

Create `cmd/mcpproxy/doctor_cmd.go`:

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"time"

	"github.com/spf13/cobra"
	"go.uber.org/zap"

	"github.com/smart-mcp-proxy/mcpproxy-go/internal/cliclient"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/socket"
)

var (
	doctorCmd = &cobra.Command{
		Use:   "doctor",
		Short: "Run health checks to identify issues",
		Long: `Run comprehensive health checks on MCPProxy to identify:
- Upstream server connection errors
- OAuth authentication requirements
- Missing secrets
- Runtime warnings
- Docker isolation status

This is the first command to run when debugging server issues.

Examples:
  mcpproxy doctor
  mcpproxy doctor --output=json`,
		RunE: runDoctor,
	}

	// Command flags
	doctorOutput    string
	doctorLogLevel  string
	doctorConfigPath string
)

// GetDoctorCommand returns the doctor command
func GetDoctorCommand() *cobra.Command {
	return doctorCmd
}

func init() {
	doctorCmd.Flags().StringVarP(&doctorOutput, "output", "o", "pretty", "Output format (pretty, json)")
	doctorCmd.Flags().StringVarP(&doctorLogLevel, "log-level", "l", "warn", "Log level")
	doctorCmd.Flags().StringVarP(&doctorConfigPath, "config", "c", "", "Path to config file")
}

func runDoctor(_ *cobra.Command, _ []string) error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Load configuration
	globalConfig, err := loadDoctorConfig()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error loading configuration: %v\n", err)
		return err
	}

	// Create logger
	logger, err := createDoctorLogger(doctorLogLevel)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error creating logger: %v\n", err)
		return err
	}

	// Check if daemon is running
	if !shouldUseDoctorDaemon(globalConfig.DataDir) {
		return fmt.Errorf("doctor requires running daemon. Start with: mcpproxy serve")
	}

	logger.Info("Fetching diagnostics from daemon")
	return runDoctorClientMode(ctx, globalConfig.DataDir, logger)
}

func shouldUseDoctorDaemon(dataDir string) bool {
	socketPath := socket.DetectSocketPath(dataDir)
	return socket.IsSocketAvailable(socketPath)
}

func runDoctorClientMode(ctx context.Context, dataDir string, logger *zap.Logger) error {
	socketPath := socket.DetectSocketPath(dataDir)
	client := cliclient.NewClient(socketPath, logger.Sugar())

	// Call GET /api/v1/diagnostics
	diag, err := client.GetDiagnostics(ctx)
	if err != nil {
		return fmt.Errorf("failed to get diagnostics from daemon: %w", err)
	}

	return outputDiagnostics(diag)
}

func outputDiagnostics(diag map[string]interface{}) error {
	switch doctorOutput {
	case "json":
		output, err := json.MarshalIndent(diag, "", "  ")
		if err != nil {
			return fmt.Errorf("failed to format output: %w", err)
		}
		fmt.Println(string(output))
	case "pretty":
	default:
		// TODO: Parse and format the diagnostics data from the API
		// The API endpoint exists and returns real data (see JSON output)
		// Need to implement pretty formatting for: upstream_errors, oauth_required,
		// missing_secrets, runtime_warnings, total_issues
		// Reference design doc lines 700-764 for intended output format

		// Placeholder output until pretty formatting is implemented:
		fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
		fmt.Println("🔍 MCPProxy Health Check")
		fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
		fmt.Println()
		fmt.Println("✅ All systems operational! No issues detected.")
		fmt.Println()
		fmt.Println("(Pretty output formatting not yet implemented - use --output=json to see full diagnostics)")
		fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
	}

	return nil
}

func loadDoctorConfig() (*config.Config, error) {
	if doctorConfigPath != "" {
		return config.LoadFromFile(doctorConfigPath)
	}
	return config.Load()
}

func createDoctorLogger(level string) (*zap.Logger, error) {
	var zapLevel zap.AtomicLevel
	switch level {
	case "trace", "debug":
		zapLevel = zap.NewAtomicLevelAt(zap.DebugLevel)
	case "info":
		zapLevel = zap.NewAtomicLevelAt(zap.InfoLevel)
	case "warn":
		zapLevel = zap.NewAtomicLevelAt(zap.WarnLevel)
	case "error":
		zapLevel = zap.NewAtomicLevelAt(zap.ErrorLevel)
	default:
		zapLevel = zap.NewAtomicLevelAt(zap.WarnLevel)
	}

	cfg := zap.Config{
		Level:            zapLevel,
		Development:      false,
		Encoding:         "console",
		EncoderConfig:    zap.NewDevelopmentEncoderConfig(),
		OutputPaths:      []string{"stderr"},
		ErrorOutputPaths: []string{"stderr"},
	}

	return cfg.Build()
}
```

**Step 2: Add GetDiagnostics method to cliclient**

Add to `internal/cliclient/client.go` (API endpoint `/api/v1/diagnostics` already exists and returns real data):

```go
// GetDiagnostics retrieves diagnostics information from daemon.
func (c *Client) GetDiagnostics(ctx context.Context) (map[string]interface{}, error) {
	url := c.baseURL + "/api/v1/diagnostics"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to call diagnostics API: %w", err)
	}
	defer resp.Body.Close()

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(bodyBytes))
	}

	var apiResp struct {
		Success bool                   `json:"success"`
		Data    map[string]interface{} `json:"data"`
		Error   string                 `json:"error"`
	}

	if err := json.Unmarshal(bodyBytes, &apiResp); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if !apiResp.Success {
		return nil, fmt.Errorf("API call failed: %s", apiResp.Error)
	}

	return apiResp.Data, nil
}
```

**Step 3: Register doctor command in main.go**

Modify `cmd/mcpproxy/main.go`, add after other command registrations:

```go
// Add after upstreamCmd := GetUpstreamCommand()
doctorCmd := GetDoctorCommand()

// Add after rootCmd.AddCommand(upstreamCmd)
rootCmd.AddCommand(doctorCmd)
```

**Step 4: Test manually**

```bash
go build -o mcpproxy ./cmd/mcpproxy

# Test (should error without daemon)
./mcpproxy doctor

# Expected: Error message about requiring daemon
```

**Step 5: Commit**

```bash
git add cmd/mcpproxy/doctor_cmd.go cmd/mcpproxy/main.go internal/cliclient/client.go
git commit -m "feat: add 'mcpproxy doctor' command

- JSON output (--output=json) fully functional with real diagnostics data
- Pretty output is placeholder - needs implementation for parsing/formatting
- Connects to existing /api/v1/diagnostics endpoint
- Requires daemon to be running"
```

---

## Task 6: Add brief CLAUDE.md documentation

**Files:**
- Modify: `CLAUDE.md`

**Step 1: Add CLI Management Commands section**

Add to `CLAUDE.md` after the "### Testing" section (around line 150):

```markdown
### CLI Management Commands

MCPProxy provides CLI commands for managing upstream servers and monitoring health:

```bash
mcpproxy upstream list              # List all servers
mcpproxy upstream logs <name>       # View logs (--tail, --follow)
mcpproxy upstream restart <name>    # Restart server (supports --all)
mcpproxy doctor                     # Run health checks
```

**Common workflow:**
```bash
mcpproxy doctor                     # Check overall health
mcpproxy upstream list              # Identify issues
mcpproxy upstream logs failing-srv  # View logs
mcpproxy upstream restart failing-srv
```

See [docs/cli-management-commands.md](docs/cli-management-commands.md) for complete reference.
```

**Step 2: Update Debugging Guide section**

Find the "## Debugging Guide" section in CLAUDE.md and update it:

```markdown
## Debugging Guide

### Quick Diagnostics

Run this first when debugging any issue:

```bash
mcpproxy doctor
```

This checks for:
- Upstream server connection errors
- OAuth authentication requirements
- Missing secrets
- Runtime warnings
- Docker isolation status

See [docs/cli-management-commands.md](docs/cli-management-commands.md) for detailed workflows.

### Common Commands

```bash
# Monitor logs
tail -f ~/Library/Logs/mcpproxy/main.log

# Check server status
mcpproxy upstream list

# View specific server logs
mcpproxy upstream logs github-server --tail=100

# Follow logs in real-time (requires daemon)
mcpproxy upstream logs github-server --follow

# Restart problematic server
mcpproxy upstream restart github-server
```
```

**Step 3: Commit**

```bash
git add CLAUDE.md
git commit -m "docs: add CLI management commands to CLAUDE.md"
```

---

## Task 7: Create comprehensive CLI reference documentation

**Files:**
- Create: `docs/cli-management-commands.md`

**Step 1: Create comprehensive documentation**

Create `docs/cli-management-commands.md`:

```markdown
# CLI Management Commands

This document describes the CLI commands for managing MCPProxy upstream servers and monitoring system health.

## Overview

MCPProxy provides two command groups:

1. **`mcpproxy upstream`** - Server management (list, logs, enable, disable, restart)
2. **`mcpproxy doctor`** - Health checks and diagnostics

All commands support both **daemon mode** (fast, via socket) and **standalone mode** (direct connection).

## Command Reference

### `mcpproxy upstream list`

List all configured upstream servers with connection status.

**Usage:**
```bash
mcpproxy upstream list [flags]
```

**Flags:**
- `--output, -o` - Output format (table, json) [default: table]
- `--log-level, -l` - Log level (trace, debug, info, warn, error) [default: warn]
- `--config, -c` - Path to config file

**Examples:**
```bash
# List servers with table output
mcpproxy upstream list

# JSON output for scripting
mcpproxy upstream list --output=json

# With debug logging
mcpproxy upstream list --log-level=debug
```

**Output Fields:**
- NAME - Server name
- ENABLED - Whether server is enabled
- PROTOCOL - Transport protocol (stdio, http, sse)
- CONNECTED - Connection status
- TOOLS - Number of available tools
- STATUS - Current status or error message

---

### `mcpproxy upstream logs <name>`

Display recent log entries from a specific server.

**Usage:**
```bash
mcpproxy upstream logs <server-name> [flags]
```

**Flags:**
- `--tail, -n` - Number of lines to show [default: 50]
- `--follow, -f` - Follow log output (requires daemon)
- `--log-level, -l` - Log level [default: warn]
- `--config, -c` - Path to config file

**Examples:**
```bash
# Show last 50 lines
mcpproxy upstream logs github-server

# Show last 200 lines
mcpproxy upstream logs github-server --tail=200

# Follow logs (Ctrl+C to stop)
mcpproxy upstream logs github-server --follow
```

**Behavior:**
- **Daemon mode**: Fetches logs via API from running server
- **Standalone mode**: Reads directly from log file (read-only)
- **Follow mode**: Requires running daemon, polls for new lines

---

### `mcpproxy upstream enable <name|--all>`

Enable a disabled server or all disabled servers.

**Usage:**
```bash
mcpproxy upstream enable <server-name>
mcpproxy upstream enable --all [--force]
```

**Flags:**
- `--all` - Enable all disabled servers
- `--force` - Skip confirmation prompt (for automation)

**Requirements:**
- Daemon must be running

**Examples:**
```bash
# Enable single server
mcpproxy upstream enable github-server

# Enable all servers (interactive confirmation)
mcpproxy upstream enable --all

# Enable all servers (skip prompt)
mcpproxy upstream enable --all --force
```

**Interactive Confirmation:**
```bash
$ mcpproxy upstream enable --all
⚠️  This will enable 5 server(s). Continue? [y/N]: _
```

**Non-interactive mode requires --force:**
```bash
$ echo "y" | mcpproxy upstream enable --all
Error: --all requires --force flag in non-interactive mode
```

---

### `mcpproxy upstream disable <name|--all>`

Disable a server or all enabled servers.

**Usage:**
```bash
mcpproxy upstream disable <server-name>
mcpproxy upstream disable --all [--force]
```

**Flags:**
- `--all` - Disable all enabled servers
- `--force` - Skip confirmation prompt

**Requirements:**
- Daemon must be running

**Examples:**
```bash
# Disable single server
mcpproxy upstream disable github-server

# Disable all servers (interactive confirmation)
mcpproxy upstream disable --all

# Disable all in script
mcpproxy upstream disable --all --force
```

---

### `mcpproxy upstream restart <name|--all>`

Restart a server or all enabled servers.

**Usage:**
```bash
mcpproxy upstream restart <server-name>
mcpproxy upstream restart --all
```

**Flags:**
- `--all` - Restart all enabled servers

**Requirements:**
- Daemon must be running

**Examples:**
```bash
# Restart single server
mcpproxy upstream restart github-server

# Restart all servers (no confirmation needed)
mcpproxy upstream restart --all
```

**Note:** Restart does not require confirmation as it's non-destructive.

---

### `mcpproxy doctor`

Run comprehensive health checks to identify issues.

**Usage:**
```bash
mcpproxy doctor [flags]
```

**Flags:**
- `--output, -o` - Output format (pretty, json) [default: pretty]
- `--log-level, -l` - Log level [default: warn]
- `--config, -c` - Path to config file

**Requirements:**
- Daemon must be running

**Examples:**
```bash
# Pretty output with issue categorization
mcpproxy doctor

# JSON output for scripting
mcpproxy doctor --output=json
```

**Health Checks:**
- Upstream server connection errors
- OAuth authentication requirements
- Missing secrets (unresolved references)
- Runtime warnings
- Docker isolation status

**Output:**
- Total issue count
- Categorized issues with actionable remediation steps
- Exit code 0 even if issues found (for scripting)

---

## Common Workflows

### Debugging Server Connection Issues

```bash
# 1. Run health check to see all issues
mcpproxy doctor

# 2. Check specific server status
mcpproxy upstream list

# 3. View logs for failing server
mcpproxy upstream logs failing-server --tail=100

# 4. Restart server
mcpproxy upstream restart failing-server

# 5. Verify fix
mcpproxy upstream list
```

### Monitoring Server Health

```bash
# Follow logs in terminal 1
mcpproxy upstream logs github-server --follow

# In terminal 2, trigger operations
mcpproxy call tool --tool-name=github:get_user --json_args='{"username":"octocat"}'

# Watch logs update in real-time
```

### Bulk Server Management

```bash
# List all servers first
mcpproxy upstream list

# Disable all for maintenance
mcpproxy upstream disable --all --force

# Perform maintenance...

# Re-enable all
mcpproxy upstream enable --all --force

# Verify
mcpproxy upstream list
```

---

## Daemon Mode vs Standalone Mode

### Daemon Mode (Preferred)
- **Detection**: Socket file exists at `~/.mcpproxy/mcpproxy.sock`
- **Behavior**: Uses HTTP API via Unix socket
- **Speed**: Fast (reuses existing connections)
- **Requirements**: `mcpproxy serve` running

### Standalone Mode (Fallback)
- **Detection**: No socket file found
- **Behavior**: Direct connection to servers or file reading
- **Speed**: Slower (establishes new connections)
- **Limitations**: Some commands unavailable (enable, disable, restart, follow, doctor)

### Command Availability

| Command | Daemon Mode | Standalone Mode | Notes |
|---------|-------------|-----------------|-------|
| `upstream list` | ✅ Full status | ✅ Config only | Standalone shows "unknown" |
| `upstream logs` | ✅ Via API | ✅ File read | Follow requires daemon |
| `upstream enable` | ✅ | ❌ | Requires daemon |
| `upstream disable` | ✅ | ❌ | Requires daemon |
| `upstream restart` | ✅ | ❌ | Requires daemon |
| `doctor` | ✅ | ❌ | Requires daemon |

---

## Safety Considerations

### Bulk Operations Warning

⚠️  **Safety Warning: Bulk Operations**

The `--all` flag affects all servers simultaneously:

- `disable --all` - Stops all upstream connections (reversible with `enable --all`)
- `enable --all` - Activates all servers (may trigger API calls, OAuth flows)
- `restart --all` - Reconnects all servers (may cause brief service disruption)

**Best practices:**
- Use `upstream list` first to see affected servers
- Test with single server before using `--all`
- Use `--force` in automation only when appropriate

### Interactive Confirmation

Confirmation prompt shows count:

```bash
⚠️  This will disable 12 server(s). Continue? [y/N]:
```

Shows what will be affected before proceeding.

---

## Exit Codes

- `0` - Success
- `1` - Execution failure (API error, connection failure, user declined confirmation)
- `2` - Invalid arguments or configuration (non-interactive without --force)

---

## Implementation Notes

### Architecture
- Commands in `cmd/mcpproxy/*_cmd.go`
- Client library in `internal/cliclient/client.go`
- API endpoints in `internal/httpapi/server.go`

### Adding New Commands
1. Create command file in `cmd/mcpproxy/`
2. Add methods to `internal/cliclient/client.go`
3. Register in `cmd/mcpproxy/main.go`
4. Follow existing patterns for daemon detection
5. Support both daemon and standalone modes where possible

### Testing
All commands support manual testing:
```bash
go build -o mcpproxy ./cmd/mcpproxy
./mcpproxy <command> [args]
```
```

**Step 2: Commit**

```bash
git add docs/cli-management-commands.md
git commit -m "docs: add comprehensive CLI management commands reference"
```

---

## Summary

This plan implements:

✅ **`mcpproxy upstream`** command group:
- `list` - List all servers with status (daemon + standalone)
- `logs` - View logs with tail/follow support (daemon + standalone file read)
- `enable/disable/restart` - Server lifecycle management with `--all` support (daemon only)

✅ **`mcpproxy doctor`** command:
- Placeholder for health checks (daemon only)
- Will be enhanced when diagnostics API is implemented

✅ **Interactive confirmation**:
- TTY detection for bulk enable/disable operations
- `--force` flag for automation

✅ **Documentation**:
- Brief summary in CLAUDE.md
- Comprehensive reference in docs/cli-management-commands.md

**Next steps:**
- Implement diagnostics API endpoints in `internal/httpapi`
- Add `doctor docker/oauth/secrets/upstream` subcommands
- Enhance doctor output with real health check data
