fix(extensions): retain servers for active clients

This commit is contained in:
Julian lechner committed 2026-09-24 17:10:03 +02:00
1 parent eeb437e234
commit 5c1b8fedba
2 files changed
+15 -4

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@
## Menus and reconciliation
- [x] Build update-menu checks per client when `Both` is selected. Both menus inspect Claude's installed plugins for every entry, so a Codex resource can start unchecked and the combined sync can remove it. (`scripts/lib/plugins.ps1:98`, `scripts/lib/plugins.sh:108`)
- [ ] Keep shared MCPorter servers until neither client needs them. Reconciliation processes `shared` records even when only one client is selected; uninstalling that client with an empty selection removes servers still used by the other. (`scripts/lib/managed-extensions.py:463`, `scripts/commands/uninstall.ps1:59`, `scripts/commands/uninstall.sh:77`)
- [x] Keep shared MCPorter servers until neither client needs them. Reconciliation processes `shared` records even when only one client is selected; uninstalling that client with an empty selection removes servers still used by the other. (`scripts/lib/managed-extensions.py:463`, `scripts/commands/uninstall.ps1:59`, `scripts/commands/uninstall.sh:77`)
- [ ] Manage global packages once across clients. With `--client both`, MCPorter is installed and recorded twice; QMD's global package can be removed during a Codex-only deselection while Claude still uses its plugin. (`scripts/lib/managed-extensions.py:342`, `scripts/lib/managed-extensions.py:378`, `scripts/lib/managed-extensions.py:463`)
- [x] Fix Bash `auto` semantic retrieval selection. Both `test_semantic_retrieval_device()` and its caller read `$?` after a completed `if`, losing the benchmark's exit code; an unsuitable device produces an empty choice instead of `false`. (`scripts/lib/semantic-retrieval.sh:13`, `scripts/lib/install-options.sh:52`)
- [x] Make ordinary update dry-runs use the saved/current selection. Both menus bypass selection in dry-run and select every manifest entry, so the preview can diverge from the real update. (`scripts/lib/plugins.ps1:91`, `scripts/lib/plugins.sh:97`)
+14 -3
View File
@@ -202,6 +202,8 @@ class Manager:
raise ValueError('Invalid owned CLI record.')
elif resource['kind'] == 'mcp' and (resource['client'] != 'shared' or not re.fullmatch(r'[a-z0-9-]+', resource.get('server', '')) or not re.fullmatch(r'https://[^\s]+', resource.get('url', ''))):
raise ValueError('Invalid owned MCP record.')
if 'clients' in resource and (not isinstance(resource['clients'], list) or any(client not in ('codex', 'claude') for client in resource['clients'])):
raise ValueError('Invalid shared extension clients.')
def save(self):
if not self.dry_run:
@@ -414,7 +416,7 @@ class Manager:
self.state['resources'].append(dict(client='codex', name=entry['name'], kind='qmd', package=package))
self.save()
def ensure_mcporter(self, entry):
def ensure_mcporter(self, entry, clients):
server, url = entry['mcporter_name'], entry['mcporter_url']
if not re.fullmatch(r'[a-z0-9-]+', server) or not re.fullmatch(r'https://[^\s]+', url):
raise ValueError('Invalid MCPorter server definition.')
@@ -434,11 +436,14 @@ class Manager:
return
if existing is not None and record is not None and existing.get('baseUrl') != record['url']:
raise ValueError('Owned MCPorter server changed locally; refusing to overwrite it.')
if record is not None:
record['clients'] = sorted(set(record.get('clients', ['claude', 'codex'])) | set(clients))
self.save()
if record is not None and existing is not None and not self.update:
return
self.command(['mcporter', '--config', str(self.home / '.mcporter/mcporter.json'), 'config', 'add', server, url])
if record is None:
self.state['resources'].append(dict(client='shared', name=entry['name'], kind='mcp', server=server, url=url))
self.state['resources'].append(dict(client='shared', name=entry['name'], kind='mcp', server=server, url=url, clients=sorted(clients)))
else:
record.update(server=server, url=url)
self.save()
@@ -507,6 +512,12 @@ class Manager:
continue
selected_entries = mcporter_entries if record['client'] == 'shared' else direct_entries
if (action == 'sync' and record['name'] not in selected_entries) or (action == 'remove' and record['name'] in selected):
if record['kind'] == 'mcp':
remaining = [client for client in record.get('clients', ['claude', 'codex']) if client not in clients]
if remaining:
record['clients'] = remaining
self.save()
continue
self.remove(record)
if action != 'remove':
for client in clients:
@@ -515,7 +526,7 @@ class Manager:
if client == 'codex' and entry.get('codex_plugin') == 'caveman@thinkhome-caveman':
self.repair_caveman_windows_hooks()
for entry in mcporter_entries.values():
self.ensure_mcporter(entry)
self.ensure_mcporter(entry, clients)
print('EXTENSIONS: reconciliation complete' + (' (dry run)' if self.dry_run else ''))