diff --git a/cmd/mcpproxy/upstream_cmd.go b/cmd/mcpproxy/upstream_cmd.go index a6fa451a..307f8aae 100644 --- a/cmd/mcpproxy/upstream_cmd.go +++ b/cmd/mcpproxy/upstream_cmd.go @@ -134,6 +134,7 @@ Supported formats: - Gemini CLI: ~/.gemini/settings.json The format is auto-detected from file content. Imported servers are quarantined by default. +Use --no-quarantine to skip quarantine for trusted configs. Examples: # Import all servers from Claude Desktop config @@ -146,7 +147,10 @@ Examples: mcpproxy upstream import --server github ~/.cursor/mcp.json # Force format detection - mcpproxy upstream import --format claude-desktop config.json`, + mcpproxy upstream import --format claude-desktop config.json + + # Import without quarantine (trusted configs) + mcpproxy upstream import --no-quarantine ~/Library/Application\ Support/Claude/claude_desktop_config.json`, Args: cobra.ExactArgs(1), RunE: runUpstreamImport, } @@ -173,9 +177,10 @@ Examples: upstreamRemoveIfExists bool // Import command flags - upstreamImportServer string - upstreamImportFormat string - upstreamImportDryRun bool + upstreamImportServer string + upstreamImportFormat string + upstreamImportDryRun bool + upstreamImportNoQuarantine bool ) // GetUpstreamCommand returns the upstream command for adding to the root command. @@ -236,6 +241,7 @@ func init() { upstreamImportCmd.Flags().StringVarP(&upstreamImportServer, "server", "s", "", "Import only a specific server by name") upstreamImportCmd.Flags().StringVar(&upstreamImportFormat, "format", "", "Force format (claude-desktop, claude-code, cursor, codex, gemini)") upstreamImportCmd.Flags().BoolVar(&upstreamImportDryRun, "dry-run", false, "Preview import without making changes") + upstreamImportCmd.Flags().BoolVar(&upstreamImportNoQuarantine, "no-quarantine", false, "Don't quarantine imported servers (use with caution)") } func runUpstreamList(_ *cobra.Command, _ []string) error { @@ -1345,8 +1351,9 @@ func runUpstreamImport(_ *cobra.Command, args []string) error { // Build import options opts := &configimport.ImportOptions{ - Preview: upstreamImportDryRun, - Now: time.Now(), + Preview: upstreamImportDryRun, + SkipQuarantine: upstreamImportNoQuarantine, + Now: time.Now(), } // Parse format hint if provided @@ -1398,7 +1405,7 @@ func runUpstreamImport(_ *cobra.Command, args []string) error { return outputImportResultStructured(result, outputFormat) } - return outputImportResultTable(result, upstreamImportDryRun, globalConfig) + return outputImportResultTable(result, upstreamImportDryRun, upstreamImportNoQuarantine, globalConfig) } // parseImportFormat converts a format string to ConfigFormat @@ -1456,6 +1463,8 @@ func buildImportedServersOutput(imported []*configimport.ImportedServer) []map[s "url": s.Server.URL, "command": s.Server.Command, "args": s.Server.Args, + "enabled": s.Server.Enabled, + "quarantined": s.Server.Quarantined, "source_format": s.SourceFormat, "original_name": s.OriginalName, "fields_skipped": s.FieldsSkipped, @@ -1466,7 +1475,7 @@ func buildImportedServersOutput(imported []*configimport.ImportedServer) []map[s } // outputImportResultTable outputs the import result in table format -func outputImportResultTable(result *configimport.ImportResult, dryRun bool, globalConfig *config.Config) error { +func outputImportResultTable(result *configimport.ImportResult, dryRun bool, noQuarantine bool, globalConfig *config.Config) error { // Header if dryRun { fmt.Println("🔍 DRY RUN - No changes will be made") @@ -1554,7 +1563,11 @@ func outputImportResultTable(result *configimport.ImportResult, dryRun bool, glo if err != nil { return err } - fmt.Println("🔒 New servers are quarantined by default. Approve them in the web UI.") + if noQuarantine { + fmt.Println("✅ Servers imported without quarantine. They are ready to use.") + } else { + fmt.Println("🔒 New servers are quarantined by default. Approve them in the web UI.") + } } return nil @@ -1593,8 +1606,8 @@ func applyImportedServersDaemonMode(ctx context.Context, dataDir string, importe Protocol: s.Server.Protocol, } - // All imported servers are quarantined - quarantined := true + // Use the quarantine state from the import result (controlled by --no-quarantine flag) + quarantined := s.Server.Quarantined req.Quarantined = &quarantined _, err := client.AddServer(ctx, req) diff --git a/docs/cli/command-reference.md b/docs/cli/command-reference.md index d495f1a1..720bf441 100644 --- a/docs/cli/command-reference.md +++ b/docs/cli/command-reference.md @@ -196,20 +196,20 @@ mcpproxy upstream disable ## Configuration Import -### import +### upstream import Import MCP server configurations from other AI tools: ```bash -mcpproxy import [flags] +mcpproxy upstream import [flags] ``` | Flag | Description | Default | |------|-------------|---------| -| `--path` | Path to configuration file | - | +| `--server, -s` | Import only a specific server by name | all | | `--format` | Force format (claude-desktop, claude-code, cursor, codex, gemini) | auto-detect | -| `--servers` | Comma-separated list of server names to import | all | -| `--preview` | Preview without importing | `false` | +| `--dry-run` | Preview import without making changes | `false` | +| `--no-quarantine` | Don't quarantine imported servers (use with caution) | `false` | **Supported Formats:** @@ -225,19 +225,22 @@ mcpproxy import [flags] ```bash # Import from Claude Desktop config -mcpproxy import --path ~/Library/Application\ Support/Claude/claude_desktop_config.json +mcpproxy upstream import ~/Library/Application\ Support/Claude/claude_desktop_config.json # Import from Claude Code config -mcpproxy import --path ~/.claude.json +mcpproxy upstream import ~/.claude.json # Preview without importing -mcpproxy import --path config.json --preview +mcpproxy upstream import --dry-run config.json # Import with format hint (if auto-detect fails) -mcpproxy import --path config.json --format claude-desktop +mcpproxy upstream import --format claude-desktop config.json -# Import only specific servers -mcpproxy import --path config.json --servers "github-server,filesystem" +# Import only a specific server +mcpproxy upstream import --server github-server config.json + +# Import without quarantine (trusted configs) +mcpproxy upstream import --no-quarantine ~/Library/Application\ Support/Claude/claude_desktop_config.json ``` **Canonical Config Paths:** @@ -251,7 +254,7 @@ mcpproxy import --path config.json --servers "github-server,filesystem" | Gemini CLI | `~/.gemini/settings.json` | `~/.gemini/settings.json` | `~/.gemini/settings.json` | :::note Imported servers are quarantined -For security, all imported servers are quarantined by default. Review and approve them before enabling. +For security, all imported servers are quarantined by default. Use `--no-quarantine` to skip quarantine for configs you trust. ::: See [Configuration Import](/features/config-import) for Web UI and REST API documentation. diff --git a/docs/features/config-import.md b/docs/features/config-import.md index dbff73fe..8134061a 100644 --- a/docs/features/config-import.md +++ b/docs/features/config-import.md @@ -62,23 +62,26 @@ For quick imports without file access: ```bash # Import from Claude Desktop config -mcpproxy import --path ~/Library/Application\ Support/Claude/claude_desktop_config.json +mcpproxy upstream import ~/Library/Application\ Support/Claude/claude_desktop_config.json # Import from Claude Code config -mcpproxy import --path ~/.claude.json +mcpproxy upstream import ~/.claude.json # Import with format hint (if auto-detect fails) -mcpproxy import --path config.json --format claude-desktop +mcpproxy upstream import --format claude-desktop config.json # Preview without importing -mcpproxy import --path config.json --preview +mcpproxy upstream import --dry-run config.json + +# Import without quarantine (trusted configs) +mcpproxy upstream import --no-quarantine ~/Library/Application\ Support/Claude/claude_desktop_config.json ``` ### Import Specific Servers ```bash -# Import only specific servers by name -mcpproxy import --path config.json --servers "github-server,filesystem" +# Import only a specific server by name +mcpproxy upstream import --server github-server config.json ``` ## REST API diff --git a/internal/configimport/import.go b/internal/configimport/import.go index f25f9612..365b0969 100644 --- a/internal/configimport/import.go +++ b/internal/configimport/import.go @@ -113,6 +113,11 @@ func Import(content []byte, opts *ImportOptions) (*ImportResult, error) { // Map to ServerConfig serverConfig, skipped, warnings := MapToServerConfig(parsed, opts.Now) + // Override quarantine if SkipQuarantine is set + if opts.SkipQuarantine { + serverConfig.Quarantined = false + } + // Create imported server imported := &ImportedServer{ Server: serverConfig, diff --git a/internal/configimport/import_test.go b/internal/configimport/import_test.go index ff518017..240b2e0a 100644 --- a/internal/configimport/import_test.go +++ b/internal/configimport/import_test.go @@ -247,6 +247,53 @@ func TestGetAvailableServerNames(t *testing.T) { } } +func TestImport_SkipQuarantine(t *testing.T) { + now := time.Date(2026, 1, 17, 12, 0, 0, 0, time.UTC) + + t.Run("default_quarantined", func(t *testing.T) { + content, err := os.ReadFile("testdata/claude_desktop.json") + if err != nil { + t.Fatalf("failed to read test file: %v", err) + } + + result, err := Import(content, &ImportOptions{Now: now}) + if err != nil { + t.Fatalf("Import() error = %v", err) + } + + for _, s := range result.Imported { + if !s.Server.Quarantined { + t.Errorf("server %s should be quarantined by default", s.Server.Name) + } + } + }) + + t.Run("skip_quarantine", func(t *testing.T) { + content, err := os.ReadFile("testdata/claude_desktop.json") + if err != nil { + t.Fatalf("failed to read test file: %v", err) + } + + result, err := Import(content, &ImportOptions{ + SkipQuarantine: true, + Now: now, + }) + if err != nil { + t.Fatalf("Import() error = %v", err) + } + + if result.Summary.Imported == 0 { + t.Fatal("expected at least one imported server") + } + + for _, s := range result.Imported { + if s.Server.Quarantined { + t.Errorf("server %s should NOT be quarantined when SkipQuarantine=true", s.Server.Name) + } + } + }) +} + func TestImport_DuplicateWithinSameImport(t *testing.T) { // Create a config with duplicate names that would result after sanitization content := []byte(`{ diff --git a/internal/configimport/types.go b/internal/configimport/types.go index c63b6cfe..e4046096 100644 --- a/internal/configimport/types.go +++ b/internal/configimport/types.go @@ -155,6 +155,10 @@ type ImportOptions struct { // ExistingServers is used to check for duplicates ExistingServers []string + // SkipQuarantine if true, imported servers are not quarantined. + // By default, all imported servers are quarantined for security review. + SkipQuarantine bool + // Now is the timestamp to use for Created field (default: time.Now()) Now time.Time }