Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions cmd/mcpproxy/upstream_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
}
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 15 additions & 12 deletions docs/cli/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,20 +196,20 @@ mcpproxy upstream disable <server-name>

## Configuration Import

### import
### upstream import

Import MCP server configurations from other AI tools:

```bash
mcpproxy import [flags]
mcpproxy upstream import <path> [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:**

Expand All @@ -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:**
Expand All @@ -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.
Expand Down
15 changes: 9 additions & 6 deletions docs/features/config-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions internal/configimport/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions internal/configimport/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`{
Expand Down
4 changes: 4 additions & 0 deletions internal/configimport/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down