refactor: introduce ClientSpec adapter; route Claude Desktop + Code through it (#5)
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Lint (ruff) (pull_request) Successful in 21s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 32s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 31s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 35s
CI / Catalog signature (pull_request) Successful in 26s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Lint (ruff) (pull_request) Successful in 21s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 32s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 31s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 35s
CI / Catalog signature (pull_request) Successful in 26s
Cross-client support (Cursor / Windsurf / VS Code) was blocked on Claude's layout being hard-coded throughout the code: servers always under the literal "mcpServers" key, a fixed set of Claude file locations, and "which client is this?" answered by sniffing a filename. Adding a client that differs on any of those axes meant chasing those assumptions through a dozen sites. This is phase 1 of #5: the keystone refactor, with NO behaviour change. It adds a ClientSpec adapter that captures the three things that vary across clients -- the top-level servers key, the config discovery paths, and the per-server value shape -- plus the capability flags that were previously computed inline from a filename (does the client expand ${VAR}? can we offer Restart?). - ClientSpec (frozen dataclass): servers_key, disabled_key, config_filename, expands_env_refs, supports_restart, and entry_to_internal/entry_from_internal -- the per-server translation seam, identity for any mcpServers-shaped client, the single point a differently-shaped client (VS Code's type/inputs form) overrides. - CLAUDE_DESKTOP and CLAUDE_CODE specs; both use mcpServers + the existing parking key, so their translation is the identity and nothing changes for today's users. resolve_client(path) reproduces the old filename rule exactly; each Profile now carries its resolved .client. - extract_servers / apply_servers / _server_sections / external_change_summary take an optional spec and default to Claude's layout, so every existing call site and test that omits a spec is byte-for-byte unchanged. The cardinal rule now generalises: apply_servers only ever writes the client's own two keys, parameterised rather than hard-coded. - profile_targets_claude_desktop and client_expands_env_refs are now thin reads off the profile's spec -- one source of truth for client identity instead of scattered filename checks -- with identical answers. - GUI: the load, Copy-to, save and stale-merge paths pass the profile's spec into the core calls. Mechanical; no logic moved into bcc.py (which CI can't test -- no PySide6). Tests: +14. Existing suite unchanged and green (behaviour preservation). A synthetic non-mcpServers spec ("servers" key, a different disabled key, a per-server `type` field) exercises the whole pipeline -- extract, apply, masking, external-change diff -- proving the seam actually generalises before any real client depends on it. 458 passed, 1 skipped; ruff clean. Refs #5 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
This commit is contained in:
@@ -2120,7 +2120,7 @@ class MainWindow(QMainWindow):
|
||||
# but keep it inside the guard: a load failure must leave the previously
|
||||
# loaded profile intact instead of half-swapping the window's state.
|
||||
try:
|
||||
servers = core.extract_servers(self.full_config)
|
||||
servers = core.extract_servers(self.full_config, profile.client)
|
||||
except Exception as exc: # pragma: no cover - defence in depth
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
@@ -2590,7 +2590,7 @@ class MainWindow(QMainWindow):
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Copy failed", f"Couldn't read {dest.label}:\n{e}")
|
||||
return
|
||||
existing = core.extract_servers(dest_cfg)
|
||||
existing = core.extract_servers(dest_cfg, dest.client)
|
||||
names = {s.name for s in existing}
|
||||
if src.name in names:
|
||||
ans = QMessageBox.question(
|
||||
@@ -2602,7 +2602,7 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
existing = [s for s in existing if s.name != src.name]
|
||||
existing.append(core.ServerEntry(src.name, dict(src.data), True))
|
||||
core.apply_servers(dest_cfg, existing)
|
||||
core.apply_servers(dest_cfg, existing, dest.client)
|
||||
try:
|
||||
backup = core.write_config(dest.path, dest_cfg)
|
||||
except Exception as e:
|
||||
@@ -2662,7 +2662,7 @@ class MainWindow(QMainWindow):
|
||||
and disk_stat != self._loaded_stat
|
||||
):
|
||||
changed_keys, server_diff = core.external_change_summary(
|
||||
self.full_config, self.current_profile.path
|
||||
self.full_config, self.current_profile.path, self.current_profile.client
|
||||
)
|
||||
dlg = StaleDialog(self, str(self.current_profile.path), changed_keys, server_diff)
|
||||
if not dlg.exec():
|
||||
@@ -2678,7 +2678,7 @@ class MainWindow(QMainWindow):
|
||||
# changed in this session (named sets), which apply_servers
|
||||
# doesn't write. Carry them over before saving (#73).
|
||||
contested = core.carry_owned_keys(self.full_config, fresh)
|
||||
core.apply_servers(fresh, self.servers)
|
||||
core.apply_servers(fresh, self.servers, self.current_profile.client)
|
||||
try:
|
||||
backup = core.write_config(self.current_profile.path, fresh)
|
||||
except Exception as e:
|
||||
@@ -2703,7 +2703,7 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
# else OVERWRITE: fall through to normal write
|
||||
|
||||
core.apply_servers(self.full_config, self.servers)
|
||||
core.apply_servers(self.full_config, self.servers, self.current_profile.client)
|
||||
try:
|
||||
backup = core.write_config(self.current_profile.path, self.full_config)
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user