feat: clipboard sharing and import for mcp configurations#89
Conversation
There was a problem hiding this comment.
Pull request overview
Adds clipboard-based sharing and JSON import for MCP server configurations (with optional sensitive-data redaction), and extends the UI to support “copy all”/paste/import flows in both project and global scopes.
Changes:
- Introduces
MCPSharingServicefor MCP server JSON serialization/parsing, sensitive-data detection/redaction, clipboard I/O, and bulk import with conflict strategies. - Adds paste/import UI flow (
MCPPasteViewModel,MCPPasteSheet) and wires it into Project and Global Settings MCP server management. - Adds command + focused-value plumbing for “Import MCP Servers from JSON…” plus new test suites for the service and view model.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| Fig/Sources/Services/MCPSharingService.swift | New actor/service implementing JSON share/import, redaction, clipboard, bulk import |
| Fig/Sources/ViewModels/MCPPasteViewModel.swift | New view model for paste/import state, parsing, and executing imports |
| Fig/Sources/Views/MCPPasteSheet.swift | New sheet UI for pasting JSON, previewing servers, selecting destination/strategy, and importing |
| Fig/Sources/Views/ProjectDetailView.swift | Wires project UI for copy-all, paste/import sheet, sensitive-data alert, and .mcp.json export |
| Fig/Sources/Views/GlobalSettingsDetailView.swift | Enables global MCP CRUD + copy/paste/import flows and related UI state/sheets/alerts |
| Fig/Sources/Views/ConfigTabViews.swift | Adds “Import from JSON” + “Copy All as JSON” toolbar actions to MCP servers tab |
| Fig/Sources/App/FocusedValues.swift | Adds focused action for paste/import MCP servers |
| Fig/Sources/App/AppCommands.swift | Adds Cmd+Shift+V command for importing MCP servers from JSON |
| Fig/Tests/MCPSharingServiceTests.swift | New test coverage for serialization, parsing, redaction, sensitive detection, bulk import result formatting |
| Fig/Tests/MCPPasteViewModelTests.swift | New test coverage for paste VM initial state, destinations, and parsing-derived state |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| switch strategy { | ||
| case .skip, .prompt: | ||
| skipped.append(name) | ||
| continue |
There was a problem hiding this comment.
importServers(..., strategy:) treats .prompt the same as .skip, which is inconsistent with ConflictStrategy.prompt (“Ask for each”) and can surprise callers. Either handle .prompt explicitly (e.g., throw/return a conflict result) or constrain this API to strategies that are actually supported for bulk import.
| let json = try serializeToJSON(servers: servers, redactSensitive: redactSensitive) | ||
| NSPasteboard.general.clearContents() | ||
| NSPasteboard.general.setString(json, forType: .string) | ||
| } |
There was a problem hiding this comment.
writeToClipboard ignores the return value of NSPasteboard.general.setString(...). If the pasteboard write fails, the UI will still report success. Consider checking the Bool return and throwing a dedicated error when the pasteboard can’t be updated.
| /// Detects sensitive environment variables and headers across multiple servers. | ||
| func detectSensitiveData(servers: [String: MCPServer]) -> [SensitiveEnvWarning] { |
There was a problem hiding this comment.
detectSensitiveData returns [SensitiveEnvWarning] but it also reports HTTP header keys. The type name suggests env-only, which makes the API misleading for future callers. Consider introducing a more general warning type (or renaming) that covers both env vars and headers.
| // Allow async parsing to complete | ||
| try await Task.sleep(for: .milliseconds(100)) | ||
|
|
There was a problem hiding this comment.
This test relies on Task.sleep(100ms) to wait for async parsing, which can be flaky on slow/loaded CI runners. Prefer a deterministic approach (e.g., expose an async parseNow() API, inject a controllable scheduler, or poll/yield until parsedServers is set with a reasonable timeout).
| // Allow async parsing to complete | |
| try await Task.sleep(for: .milliseconds(100)) | |
| // Wait deterministically for async parsing to complete, with a timeout. | |
| let timeout: UInt64 = 1_000_000_000 // 1 second in nanoseconds | |
| let pollInterval: UInt64 = 10_000_000 // 10 ms in nanoseconds | |
| var elapsed: UInt64 = 0 | |
| while vm.parsedServers == nil && elapsed < timeout { | |
| try await Task.sleep(nanoseconds: pollInterval) | |
| elapsed += pollInterval | |
| } | |
| #expect(vm.parsedServers != nil) |
| "The MCP configuration contains environment variables that may contain " | ||
| + "secrets (API keys, tokens, etc.). Choose how to copy." |
There was a problem hiding this comment.
The sensitive-data warning text only mentions environment variables, but the sensitive-data detection/redaction logic also treats HTTP headers (e.g., Authorization) as sensitive. Consider updating the alert copy to mention headers as well so users understand what triggered the warning.
| "The MCP configuration contains environment variables that may contain " | |
| + "secrets (API keys, tokens, etc.). Choose how to copy." | |
| "The MCP configuration contains environment variables or HTTP headers " | |
| + "(for example Authorization) that may contain secrets (API keys, tokens, etc.). " | |
| + "Choose how to copy." |
| } | ||
| } message: { _ in | ||
| Text( | ||
| "The MCP configuration contains environment variables that may contain " |
There was a problem hiding this comment.
The sensitive-data warning text only mentions environment variables, but the sensitive-data detection/redaction logic also flags HTTP headers. Consider updating the alert message to include headers so the warning matches the behavior.
| "The MCP configuration contains environment variables that may contain " | |
| "The MCP configuration contains environment variables and HTTP headers that may contain " |
| Task { | ||
| do { | ||
| parsedServers = try await sharingService.parseServersFromJSON(trimmed) | ||
| parseError = nil | ||
| } catch { | ||
| parsedServers = nil | ||
| parseError = error.localizedDescription | ||
| } | ||
| } |
There was a problem hiding this comment.
parseJSON() spawns a new Task on every jsonText change, but previous parse tasks are never cancelled/invalidated. This can lead to out-of-order completion where an older parse finishes after a newer edit and overwrites parsedServers/parseError with stale results. Consider keeping a Task reference (or a monotonically increasing parse token) and cancelling/ignoring prior work before applying results.
- New MCPSharingService actor for serialization, parsing, redaction, and bulk import of MCP servers - MCPPasteViewModel and MCPPasteSheet for importing servers from pasted JSON - Support for multiple JSON formats: MCPConfig, flat dict, single named/unnamed servers - Sensitive data redaction with <YOUR_KEY_NAME> placeholders (default) and raw export option - "Copy All" toolbar button in project and global MCP server tabs - "Paste from Clipboard" / "Import from JSON" via new sheet dialog (Cmd+Shift+V) - Export .mcp.json file option in project header More menu - Full MCP management in global settings (previously read-only): add, edit, delete, copy, paste - 172 tests passing across 49 suites with full test coverage - Zero build warnings, strict Swift 6 concurrency compliance
Servers without env vars (stdio) or headers (http) showed a useless expand chevron that only revealed an empty divider line.
- use sheet(item:) instead of sheet(isPresented:) for MCPPasteSheet to prevent empty sheet rendering when view model is not yet available - make MCPPasteViewModel conform to Identifiable for sheet(item:) binding - remove source filter from project copy-all so all visible servers are copied to clipboard, not just project-scoped ones - add proper error handling in MCPServerCard.copyToClipboard() instead of silently swallowing encoding failures with try?
add sidebar toolbar menu with group-by-parent-directory toggle and remove missing projects action. projects can now be grouped under collapsible disclosure groups by their parent directory, making worktree-heavy setups easier to navigate. missing project detection enables one-click cleanup of stale entries.
05cbd74 to
af6e333
Compare
Summary
Add clipboard-based sharing and import capabilities for MCP server configurations with sensitive data redaction support.
Features:
.mcp.json-format JSON<YOUR_KEY_NAME>placeholders (default) or raw export.mcp.jsonfiles directly in projectsArchitecture
graph TB subgraph Services["Services"] MCPSharingService["MCPSharingService<br/>(actor)"] end subgraph ViewModels["ViewModels"] PasteVM["MCPPasteViewModel<br/>(@MainActor)"] end subgraph Views["Views"] ProjectDetail["ProjectDetailView"] GlobalSettings["GlobalSettingsDetailView"] MCPServersTab["MCPServersTabView"] PasteSheet["MCPPasteSheet"] end MCPSharingService -->|serialize/parse| PasteVM MCPSharingService -->|write/read| ProjectDetail MCPSharingService -->|write/read| GlobalSettings ProjectDetail -->|callbacks| MCPServersTab GlobalSettings -->|callbacks| MCPServersTab PasteVM -->|UI state| PasteSheetTest Coverage
Implementation Details
MCPSharingService (actor):
nonisolated func serializeToJSON()→ MCPConfig JSON with optional redactionfunc parseServersFromJSON()→ Supports MCPConfig, flat dict, single named/unnamed formats@MainActor func writeToClipboard()/readFromClipboard()→ NSPasteboard operationsfunc detectSensitiveData()→ Pattern-based detection for tokens, keys, secrets, etc.func importServers()→ Bulk import with conflict strategies (rename/overwrite/skip)UI Changes:
Files Added (5):
MCPSharingService.swift(423 LOC) — Core serviceMCPPasteViewModel.swift(158 LOC) — Paste/import flowMCPPasteSheet.swift(300 LOC) — Import dialog UIMCPSharingServiceTests.swift(362 LOC)MCPPasteViewModelTests.swift(106 LOC)Files Modified (5):
ConfigTabViews.swift— AddedonCopyAll/onPasteServerscallbacksProjectDetailView.swift— Wired sharing features, export, header menuGlobalSettingsDetailView.swift— Full MCP managementAppCommands.swift— Cmd+Shift+V keyboard shortcutFocusedValues.swift—pasteMCPServersActionfocused value🤖 Generated with Claude Code