Native menu bar (macOS) and system tray (Windows) apps for managing the custom MCP connectors in Claude Desktop's configuration — with automatic backups of every change they make.
Claude Desktop reads its MCP servers from claude_desktop_config.json
(~/Library/Application Support/Claude/ on a Mac, %APPDATA%\Claude\ on
Windows), a file you otherwise maintain by hand and that Claude itself has
been known to overwrite or wipe
(#32345,
#56296,
#37286). Connector
Control keeps its own master list as the source of truth, treats Claude's
config as generated output, and backs up each file it manages — Claude's
config, the master list and collections.json — before writing to it, so a
wiped or mangled config is always one click from restored.
Pending: equivalent Windows tray-flyout screenshots.
- One-click enable/disable — toggle any connector from the menu bar or tray; changes apply to Claude's config immediately, and a Restart Required button appears until Claude is running the new config (derived from Claude's actual process launch time, so it clears no matter how Claude restarts).
- Full editor — form view for the common cases (remote
mcp-remoteservers get a simple Name + URL form; local servers get command/args/env editors with secret masking, the arguments numbered from 1 as messages count them), plus a raw JSON view with live validation and paste-a-README-snippet support. The two views stay in sync, and switching never silently loses fields the form can't represent. - Self-healing — the app watches Claude's config; if connectors vanish from it (Claude update, cloud sync, crash), it writes them back from the master list on its own and raises Restart Required, and a notification says so even when the app's window is closed.
- Automatic backups — timestamped copies of Claude's config, the master list and collections.json, each taken before that file is written (configurable retention, plus a permanent first-run snapshot), with in-app restore.
- Syncable — point the master list at a folder synced by git, iCloud, or Dropbox and share one connector catalog across machines; backups always stay machine-local so they never pollute the synced folder.
- Missing-tool warnings — a connector that starts through npx, node, uvx or uv shows a caution glyph when that tool isn't installed where Claude Desktop can find it; the editor and Settings ▸ Claude ▸ Tools say what to install, with a download link and the brew or winget command.
- Careful with secrets — connector env vars can hold API tokens, so the master list and all backups are written owner-only (mode 600 on macOS, an owner-only ACL on Windows).
The editor's two views of the same connector:
![]() Form view |
![]() JSON view |
Collections are full, independent connector snapshots — each has its own
complete list of connectors and enabled flags. The popover (Mac) or flyout
(Windows) runs the active one. A chip in its header — <collection name> ▾
— shows which collection that is and opens a menu of every collection, a
check on the active one, then Manage Collections. Choosing a collection
switches to it, which applies immediately, same as any other change, and
raises Restart Required just like a toggle would. The switches below the
chip turn the active collection's connectors on and off. That is all the
popover or flyout does: adding, editing, copying and sharing connectors
happen in the Collections window.
A collection is one of three kinds.
Local collections are the ordinary kind, and everything in them is editable. Every profile from an earlier version is now a local collection with the same name and the same connectors. The last local collection can't be deleted, so there is always somewhere to add a connector.
Subscribed collections are read-only mirrors of a collection document somebody else publishes. You fill in the values the author left for you and switch connectors on and off; nothing else can be edited, and nothing can be added — the Collections window's + above the connector list is dimmed with the tooltip "Additions go in a local collection." A chain glyph follows the collection's name on the chip and in the Collections window's sidebar, with the source file's path in its tooltip, and an amber dot joins it while an update is waiting to be reviewed. In the chip's menu on a Mac the chain is the row's icon and a waiting update reads "· update available" after the name, because a macOS menu row draws one title and one image; the Windows menu draws the chain and the dot. Every row of a subscribed collection carries a lock.
Published collections are local collections that also write their document to a folder whenever their content changes. They carry no glyph on the chip or in the sidebar; the Collections window's header marks them Published. Where the document goes is a fact about one machine, so only the machine that publishes it says so: the Collections window's ⋯ menu offers Show Published File, and the editor has a line at the top: "Published to — saving updates the file your team reads. Secrets stay here."
Manage Collections opens the Collections window. When the active collection has no connectors, the popover or flyout says "No connectors in “”." with a Manage Collections button beneath it, which opens the window on that collection. Every control in it sits on the thing it acts on. What this README calls a sheet — Import, Copy, Publish, Export, Review — opens as a dialog on Windows. Escape cancels any of these, and the connector editor, as its Cancel button does.
- The sidebar lists the collections under the heading Collections. Its + (tooltip "Add Collection") offers New Collection, which asks for a name and makes an empty local collection, leaving Claude running the active one; fill it with the list's +, then make it active once it holds what you want. Next come Import, "Adds copies you own", and Subscribe, "Stays in sync, read-only" (see Sharing a collection with a team). Selecting a collection only shows it; double-click it, press Return, or choose Make Active from its context menu, to switch to it. Double-clicking the collection that is already active does nothing. Renaming or deleting a collection other than the active one changes nothing Claude runs, so it leaves Claude's config alone and raises no Restart Required. Deleting the active collection makes the first local collection by name that has connectors active, or, when none has, the first remaining collection by name; the confirmation names it.
- The header carries the selected collection's name, its pills — a green Active on the collection Claude is running, then Published or Subscribed, never both — and a ⋯ (tooltip "More") holding everything done to the collection itself. Under the name, the header counts the collection's connectors — "5 connectors". On a subscribed collection whose source couldn't be read, the reason follows the count: "5 connectors · data-team.json couldn’t be read: …". The menu lists only what applies: Make Active first, on a collection that is not active; Rename; Duplicate, or Make Local Copy on a subscribed collection; Start Publishing, or once it publishes Publishing Settings and Stop Publishing, with Show Published File on the machine that writes the file; on a subscribed collection Refresh and Show Source File once its file has been found, and Stop Syncing, which asks first; Export All, dimmed on a subscribed collection; and Delete, which asks first and says what goes with the collection: its connectors ("Its 3 connectors are deleted with it."), but not their copies in other collections, nor a subscribed collection's source file; the collection that becomes active, when it is the active one; and, when it held connectors, that a copy remains in Backups. Duplicate copies a local collection into a new local one exactly as it stands, each connector's switch included; it is your own copy, so nothing new is marked as imported, and a connector the source held as an imported copy keeps its "Imported from" line. Make Local Copy copies a subscribed collection into a new local one, every connector switched off and marked with where it came from. Neither makes the copy active.
- The connector list has its + under the header's ⋯, beside the count (tooltip "Add Connector"). It opens the editor on a new connector in this collection. Each row has a tick — a lock on a subscribed collection — then the connector's name, a caution glyph when something needs your attention, and what the connector runs. Click a row anywhere but its tick to open its editor (read-only on a subscribed collection); a click on or near the tick ticks it. From the keyboard, the arrow keys move between rows, Return opens the editor and Space ticks. Names line up in a column as wide as the longest of them, up to a cap past which a long name is cut. There is no switch here and no right-click menu: whether a connector is on is the popover's or flyout's business, so to switch a connector in another collection, make that collection active first.
- The selection bar along the bottom is empty while nothing is ticked. Tick rows and it reads "2 selected", with Copy to, Export and, apart at the far end, Delete. The rows of a subscribed collection can't be ticked.
The column after the name says what the connector runs: a remote connector's
host (with its scheme, https://mcp.example.com, for one Claude reaches by
URL alone rather than through mcp-remote), or a local one's program with its
paths, URLs and package names — npx …/server-filesystem ~/Documents. It is
built to leave secrets out. It shows the program's name, a URL as its
scheme, host and port, a file path with your home folder shortened to ~,
and a package's name. It leaves out whatever follows a flag named like a
secret (anything with token, key, secret, pass, pwd, pw, auth, credential or
bearer in it), any KEY=value word, a URL's user name and password and its
query string, long random-looking strings, and everything it does not
recognise, flags included. It is a best-effort mask, not a guarantee: a
secret shaped like a path or a package name, or one following a flag that is
not named like a secret, still shows, as does one written into a real
hostname, or a hyphenated word in the place where npx or uvx names the
server it runs. Check the column before you share a screenshot of the
window.
Copy to lists every other collection — a subscribed one is listed but
dimmed and marked "read-only", since its connectors are the author's — then
New Collection, which asks for a name and makes an empty local
collection to take the copies. Copies arrive switched off and record where
they came from. When the destination already holds a name you ticked, the
Copy sheet asks about each clash: Replace, Keep both, which lands the
copy as <name> 2 and is the default, or Skip; the rest are listed as
"new · arrives off". Replacing a connector in the active collection applies
at once. Export opens the
Export sheet on the ticked rows. Delete asks first — "Delete
“”?", or "Delete 3 connectors?" — adding "A copy remains in Backups.";
deleting from the active collection applies at once.
A connector that another local collection holds an identical copy of opens in the editor with a checkbox, off by default: "Also apply this change to , which has an identical ". Tick it and saving makes the same change in those collections too; each copy keeps its own switch.
The master list file (mcps.json) is v2 (collection-aware); an older v1 file, from a build before 1.1, is treated as any unreadable one is (see How it works): kept aside and replaced by the newest backup that can be read, or rebuilt from Claude's current config when none can, as is usual for a file that old. Beside it, a collections.json records which collection is which kind, what each one still needs, and each published collection's settings, including the text of every path you marked. If you sync mcps.json across machines, every machine must run 1.1 or later — an older app can't parse the v2 file and will treat it as corrupt.
A collection document is a single JSON file describing a collection's connectors. Publishing writes that file to a folder and keeps it current; subscribing follows it. No network access is involved: git, OneDrive, Google Drive, Dropbox or a file you hand over carry the document, exactly as they carry the master list today.
Select a local collection in the Collections window and choose Start
Publishing from its ⋯. Point it at a folder in a repository or a synced drive — the master list's
own folder and the backups folder are refused — and the app writes
<collection-name>.json there, the name lowercased with every run of other
characters collapsed to a hyphen. The file name and the document's identity
are fixed the first time you publish and never re-derived, so renaming the
collection later does not rename the file.
The sheet decides what leaves the machine:
- Environment values · stripped unless shared lists every variable of every connector with a share value tick. Unticked, which is the default, the row takes your hint: only the variable's name and that hint travel, and each subscriber supplies the value. Ticked, the row shows the value that will travel, and the value goes into the document.
- Machine-specific paths · found in arguments lists every argument that looks like a path on this machine, each with a tick that marks it; a marked row asks for a placeholder name and a hint. Unmarked, which is the default unless this machine already keeps that path back, it travels as written. Marked, it travels as a placeholder every subscriber fills in for themselves.
- Document preview is the document itself, exactly as it will be
written. An argument or shared value that looks like a credential is
listed under the preview, each line naming the connector it came from,
and so are a header or a URL query or fragment parameter named like a
secret or holding one, a URL parameter whose name is itself a token, a URL
path segment that looks like a key, and a URL with a user name or password
in it. Each line says how the value is held: one that only refers to a
credential kept elsewhere, such as
Bearer ${API_TOKEN},Bearer $API_TOKEN(without braces, only a name in capitals, as a shell writes its variables) or a placeholder, "refers to a credential", and a URL user with no password "names a user". A connector whose value then holds the credential itself waits for review again, and so does one given a new path segment or bare parameter that looks like a key: each position is a line of its own. Nothing is ever edited on your behalf; the preview is there so you see every byte before it leaves.
Once the collection publishes, the same menu's Publishing Settings changes what is shared or re-marks a path: the sheet reopens on the folder and every tick the collection publishes with, and pressing Publish there updates what is shared and rewrites the document.
The bearer token, custom header value or OAuth client secret set in a
remote connector's Authentication fields always becomes a placeholder,
whatever you tick. A header typed straight into the arguments travels as
written, so check the preview for one; the warnings under it read it as a
header, with or without a space after the colon (Authorization:Bearer …).
Enabled flags never travel, so turning a connector on or off never rewrites
the document.
${COLLECTION_DIR} is the other way to keep a path out of a document. Write
it into a local server's command, arguments or environment values and each
subscriber's app expands it to the folder their copy of the document sits
in — so a team that keeps the document beside the server in one repository
shares one entry:
"command": "node",
"args": ["${COLLECTION_DIR}/ledger/dist/index.js"]
On the machine that publishes the collection, ${COLLECTION_DIR} stands for
the publish folder, so a tool you keep beside the document runs from there
from the moment publishing starts. The published document still carries the
token as written, and each subscriber resolves it against their own copy of
the folder. In a local collection this machine does not publish — never
published, stopped, or published from your other machine — the token has no
folder: Claude gets it unexpanded, and the connector's row says
"${COLLECTION_DIR} has no folder until this collection is published from
this Mac." ("this PC" on Windows). Stop Publishing leaves the token in place
rather than writing the folder into those connectors. A connector that moves
from a document into a local collection — imported as a copy, copied to a
local collection, or kept by Make Local Copy or Stop Syncing — gets the
folder written in place of the token, once, since a local collection has no
document to resolve it against later.
The document is rewritten whenever what the collection runs changes, by the machine that published it. That machine has to be running for a change to reach the team: an edit made on your other machine travels through the master list and is published the next time the publishing machine sees it. A write that fails puts "Couldn’t publish …" on the banner with Choose Folder, and in the popover or flyout Stop Publishing beside it (in the window, Stop Publishing is in the ⋯). The Publish sheet stays open on a failure, and pressing Publish there, or in the sheet opened again later, retries at once; otherwise the next change retries the write. The banner goes if the collection stops publishing, or is deleted, on your other machine.
An automatic publish never sends a connector you haven't reviewed in the Publish sheet, or a new credential in one you have. A connector added to a published collection, copied or imported into it, or added on your other machine, and a connector edited so that it now holds something that looks like a credential (in an argument, a shared value, a header or its URL), stop the automatic publish: the banner names the connector and offers Publishing Settings, and the document in the folder stays as it was until you press Publish there. Publish reviews what the sheet showed you: a connector that arrives while the sheet is open waits until you open it again. Other edits, renames and deletions publish on their own as before.
Export writes the same document once, wherever you choose, with the same preview and warnings. The ⋯ menu's Export All writes the whole selected collection; the selection bar's Export writes only the rows you tick. A subscribed collection is the author's document already, so Export All is dimmed on one and its rows can't be ticked.
Subscribe, under the sidebar's +, picks a document and opens the Import sheet on Keep as its own collection, in sync with this file; Import opens the same sheet on the other mode. Either way the sheet names the document and lists its connectors before anything happens; a file it cannot read shows why (" couldn’t be read: …"), with no Import button. A subscribed collection arrives with every connector switched off and does not become the active one.
What is the author's: names, commands, arguments, URLs and auth. They open locked in the editor under a grey line reading "Synced from · read-only", with a What can I change? link that spells out the rule. What is yours: the values the document asks this machine for, and which connectors are on. If the author changes or deletes a connector while its editor is open, Save refuses — "“” changed outside this editor." or "“” was deleted outside this editor." — rather than write the old version back; reopen the editor to fill in your values again.
When the author changes the file the collection says so — "Data team changed at its source: adds jira; deletes confluence." — with Review & Apply. The review sheet groups what would land under Added, Deleted and Changed, with the JSON either side of every change, and nothing lands until you press Apply. Added connectors arrive switched off, and your filled values follow their placeholder even if the author moved it to another argument. If the collection is active, applying rewrites Claude's config and raises Restart Required. Refresh re-reads the file on demand. A half-written file, or one a sync client has not fetched yet, is retried quietly before anything is reported.
Another machine picks the collection up from the master list, but where the document sits is a per-machine fact. If it lies inside the master-list folder the app finds it by itself; otherwise the collection shows "’s file isn’t on this Mac yet." — "this PC" on Windows — with Locate .
To take a subscribed collection somewhere you can edit it, choose Make Local Copy from its ⋯: it copies the whole collection into a new local one, every connector switched off. Stop Syncing turns the collection itself into an ordinary local one, keeping every connector, every value you filled in and every switch; it asks first, and says as much. Deleting a subscribed collection never touches the source file.
The Import sheet's other mode, Add to a collection, copies the
document's connectors into a local collection of your choosing and keeps no
link to the file afterwards. It starts on the collection selected in the
Collections window when that one is local, otherwise on the active collection
when that one is, and otherwise on the first local collection by name. The last choice, New Collection, asks for a name; an empty
one is refused with "Name must not be empty." and the picker goes back to
the collection it was on. The copies then go into a new, empty local
collection, which does not become active. It is made only when Import can
land: a document that can no longer be read by then makes nothing, and the
sheet says why.
Each connector is listed as "new · arrives
off", "already present · skipped", or "skipped: ". A name the
collection already holds starts unticked, as "already present · skipped";
tick it to choose Replace, the default, which "keeps your filled values",
Keep both, which lands the new one as <name> 2, or Skip. Every
copy arrives switched off, and its editor carries an italic line: "Imported
from “” on . Edits stay here."
Wherever the author stripped a value, the document carries a
${CC_NEEDS:<name>} marker in its place, and the connector waits for yours.
The row
shows a caution, the editor marks the field "needs your value" or "needs
your path", and the author's hint sits with it. Filled values are yours:
they live in your master list, they never go into a document, and they
survive every update from the source. A subscribed collection that uses
${COLLECTION_DIR} but has not found its file yet says "Locate the
collection file to resolve paths." on the rows that need it.
Every connector in a document is a command Claude runs. Anyone with write
access to the shared folder can change what Claude runs on every subscriber,
once those subscribers apply the change — the review step is the control.
A connector that runs a program kept in the shared folder through
${COLLECTION_DIR} is the exception: a change to that program reaches every
subscriber the next time Claude starts it, with nothing to review, because
the review covers the document and not the files it points at. Treat write
access to a published collection's folder as you would treat access to the
machines that follow it, including the one that publishes it.
- Stop Publishing — and deleting a published collection — asks "Also delete from the folder?", and keeping it is the default: Return and Escape both keep it, and deleting it takes a click on Delete. Stop Publishing from the failed-write banner, or while the last write is failing, keeps the file without asking. Once the file is kept, publishing that collection into the same folder again is refused with " already exists there and belongs to a different collection.": the leftover file carries the identity the collection had before, and the app never writes over a document it cannot vouch for. Delete the file from the folder, then publish again.
- A path you mark in the Publish sheet is remembered together with the text it had, so it stays a placeholder when you add, delete or reorder arguments around it, correct it in place in the editor's form, or rename the connector. If it changes somewhere the app cannot follow — the JSON view, a hand edit, or an older version of the app on any machine — and the app can no longer tell which argument it is, the app stops publishing that collection rather than send the path as written. The banner gives the reason, "A path marked in “” has moved. Open Publishing Settings to mark it again." Publishing stops the same way, each with a banner of its own, when the document would carry a path this machine keeps back, or this machine's publish folder, in some other connector. However it stops, the document already in the folder is left exactly as it was, and subscribers receive none of that collection's other changes until the entry is answered.
- Reopen Publishing Settings and the sheet lists every mark it could not place, and every path this machine keeps back that turns up elsewhere in the document, each with its connector and the field it sits in. Publish and Export stay unavailable until every entry is answered, and each kind of entry takes its own answer. A lost mark: tick the path where it now sits, or Forget Mark. A kept-back path: tick it where it sits, or Release it to let it travel as written once you have read the preview. This collection's own publish folder: Use ${COLLECTION_DIR} alone, which rewrites that connector to the token; it is never released, because a document that would carry it is never written. Another folder this machine keeps — one it publishes another collection into, or where a subscribed collection's document sits — is kept back too: its entry names that collection, and you tick it where it sits or Release it like any other path.
- A path your other machine marks, in a collection published from there, is kept back from every collection this machine publishes. It stays kept back once that machine drops the mark, stops publishing the collection or deletes it, even when the change syncs in while this machine is off: Release it in this machine's Publish sheet to let it travel.
- A path stays kept back until you Release it. Unticking its row is not enough, nor is Forget Mark (it takes the mark off the record, not the path off this machine's list), and neither is deleting the argument or the connector that held it and pressing Publish: put back later, it is refused and listed again. A Release belongs to the collection you gave it in: a collection made later under a deleted one's name, or renamed onto it, starts with none, and its sheet lists the path again. One limit: if your other machine deletes a collection and makes a new one with the same name, and both changes reach this machine in the same sync, this machine cannot tell the two apart, so the new one keeps the old one's releases and publish folders.
- A differently spelled version of a marked path is a different path to the
app — another case,
~in place of your home folder, a trailing slash. It is not recognised as the one you marked, so it travels as written; read the preview. - Restoring a backup of Claude's configuration puts it back into the collection it was taken from and makes that collection active. A backup whose collection is gone is refused: "This backup was taken from “”, which no longer exists. Nothing was restored. Create a collection named “” again, and this backup goes back into it." Only this version records the collection, so a backup from an earlier one — which is every backup you already have, and the first-run original — goes into the active collection instead. While the active collection is subscribed, such a backup is refused before anything is asked: "“” is subscribed, so its connectors are the author’s. Make a local collection active, then restore."
- An export of part of a published collection still carries that collection's identity, so your own app refuses to subscribe to it, as it refuses the published document itself.
- Replacing an imported copy keeps a filled value only where the author left the placeholder in the same place. A copy is not a subscription and has nothing recording where the value used to be; a subscribed collection does, and follows the move.
- The document's name is only a default at import. Renaming a published collection never renames a subscriber's.
Requires macOS 14 (Sonoma) or later. The app is a universal binary (Apple Silicon + Intel), Developer ID–signed and notarized by Apple, so it runs without Gatekeeper warnings.
- Download
ConnectorControl_<version>.dmgfrom the latest release. - Open it and drag Connector Control to Applications.
- Launch it — a plug icon appears in the menu bar. On first run it imports your existing connectors from Claude's config into the master list; your first change takes the permanent snapshot of the original config.
There is no dock icon; the app lives entirely in the menu bar. Enable Launch at login in Settings (⚙︎) if you want it always available. The app checks for new releases on its own and offers each one in an update window; Settings ▸ General ▸ Updates has a switch for installing them automatically instead, and a Check for Updates button.
Quit the app, then delete:
/Applications/Connector Control.app
~/Library/Application Support/Connector Control/ # master list, collections, backups
Then clear its settings with defaults delete com.dlaporte.connector-control.
Your claude_desktop_config.json keeps whatever connectors were enabled at the time — the app leaves Claude's config valid on the way out.
Requires Windows 10 version 1809 (build 17763) or later, or Windows 11, on an x64 or Arm64 PC; each has its own installer and updater, built to run natively on it. The installer and the app are code-signed. While the publisher is new to Microsoft's SmartScreen, Windows may still show "Windows protected your PC" with the publisher named; choose More info, then Run anyway. The warning goes away as the signature earns reputation.
- Download
ConnectorControl-win-x64-Setup.exe(Intel and AMD PCs) orConnectorControl-win-arm64-Setup.exe(Arm PCs) from the latest release. - Run it. It installs for the current user — no administrator prompt —
under
%LOCALAPPDATA%\ConnectorControl, adds a Start menu entry (no desktop icon), and launches the app. - A plug icon appears in the system tray; open the
^overflow if Windows tucked it away there. On first run the app imports your existing connectors from Claude's config into the master list; your first change takes the permanent snapshot of the original config.
Left-click the tray icon for the connector list; right-click it for Open, Settings and Quit Connector Control. Updates are offered, not installed silently: the app checks GitHub for new releases and shows Install and Relaunch when one is available (Settings ▸ General ▸ Updates has a switch to download and install them automatically, and a Check for Updates button). Turn on Launch at startup there to have it always available.
Turn off Launch at startup first, or its Startup entry stays behind.
Settings ▸ Apps ▸ Installed apps ▸ Connector Control ▸ Uninstall removes
the program (%LOCALAPPDATA%\ConnectorControl). App data is left in place;
delete it yourself for a clean slate:
%LOCALAPPDATA%\Connector Control\ # master list, backups, settings
Your claude_desktop_config.json keeps whatever connectors were enabled at the time — the app leaves Claude's config valid on the way out.
~/Library/Application Support/Connector Control/
├── mcps.json ← master list: every connector + enabled flag (source of truth)
├── collections.json ← collection kinds, needs and publish settings; moves with mcps.json
├── collections-local.json
│ ← this machine's document and publish folders; never synced
└── backups/ ← timestamped copies of Claude's config, mcps.json and
collections.json, rotated; machine-local
├── backup-collections.json ← which collection each backup came from
└── claude_desktop_config.original.json ← taken before the app first writes Claude's config; never pruned
~/Library/Application Support/Claude/claude_desktop_config.json
← generated output: only enabled connectors are written;
every other key in the file is preserved untouched
On Windows the same layout lives under %LOCALAPPDATA% (never the roaming
profile, so backups stay on the machine that made them):
%LOCALAPPDATA%\Connector Control\
├── mcps.json ← master list (source of truth)
├── collections.json ← collection kinds, needs and publish settings; moves with mcps.json
├── collections-local.json
│ ← this machine's document and publish folders; never synced
├── settings.json ← app settings
└── backups\ ← timestamped copies of Claude's config, mcps.json and
collections.json, rotated; machine-local
├── backup-collections.json ← which collection each backup came from
└── claude_desktop_config.original.json ← taken before the app first writes Claude's config; never pruned
%APPDATA%\Claude\claude_desktop_config.json ← generated output, as above
Every change (toggle, edit, add, delete, restore) writes the master list
and, when it alters what Claude runs, regenerates the mcpServers section of
Claude's config — atomically, after backing up each file it writes. Backups are named by the millisecond they were taken; two
taken in the same one are numbered, and still list, restore and prune
newest first. A master list that can't be read is kept aside as
mcps.corrupt.<time>.json and replaced by its newest backup that can be
read, so every collection survives; only when no backup can be read is it
rebuilt from Claude's config. A banner says which happened. A reconciliation pass runs at launch, every time the popover or flyout
opens, and whenever either file changes on disk: connectors added outside the app
are imported into the active collection, and an outside edit to a connector the app
knows, or its removal, is undone by regenerating Claude's config from the master
list, with a notification, so no connector is ever silently dropped. While a subscribed collection is active, a connector
added outside the app goes into a local collection instead: the one Claude's config
was last applied from if that is local, otherwise the first local collection by name,
or, if there is none, a new, empty one named "Default" ("Default 2" and so on if that is
taken). The subscribed collection stays as its author published it, and the notification
names where the connector went (at launch, when nothing is notified, the banner does).
Claude only reads its config at startup, hence the Restart Required flow.
On Windows, Restart Claude asks Claude Desktop to end its session cleanly
(the same request Windows sends at sign-out) and relaunches it from its Start
menu entry. Older builds of Claude Desktop kept a virtualized copy of the
config under %LOCALAPPDATA%\Packages\Claude_…\LocalCache\Roaming\Claude\;
the app manages that copy only when it exists and was written more recently
than the one under %APPDATA%, and Settings ▸ Claude lets you point it at any
file.
![]() General |
![]() Storage |
![]() Claude |
General covers launch-at-login, the confirmation prompts, outside-change notifications, and updates. Storage is where the master list lives (point it at a synced folder to share connectors across machines) and how many backups to keep. Claude lets you choose which Claude app to restart and shows whether the launchers connectors depend on are installed.
Settings ▸ Storage ▸ Master List Location ▸ choose a folder inside your synced location (a git repo, iCloud Drive, OneDrive, Dropbox). The app adopts an mcps.json already there, or seeds the folder with your current list. Other machines running Connector Control point at the same folder and pick up changes live (the file is watched). Notes:
- The whole file syncs — including enabled/disabled state.
- Connector env vars (API keys!) sync too. Use a private repo, or keep secrets out of synced connectors.
- A change that arrives through the synced folder is written into Claude's config and announced by name — which connectors it added, deleted or changed — whether or not Claude is running at the time. Every connector is a command Claude runs, so treat write access to the synced folder as you would treat access to the machines that follow it.
- Conflicts are your sync tool's department; local backups make any bad merge recoverable.
- A Mac and a PC can share one list, but connector commands are OS-specific:
a Mac writes remote connectors as
npx mcp-remote …, Windows writes them ascmd /c npx mcp-remote …, and local servers carry their own paths. Each app preserves the other platform's entries untouched — an entry may simply fail to start in Claude on the other OS until you edit it there. Because cmd.exe re-parses everything aftercmd /c, the Windows editor refuses a URL, header name, OAuth client ID or client secret containing& | < > ^ "or a space, and OAuth scopes containing any of those but a space, for such a connector; use the JSON view if you really need one. - A shared collection document behaves better across platforms: it stores remote connectors in a neutral form, so a Mac author's remote connectors start on Windows and a Windows author's start on a Mac, each app writing its own launcher. Local servers travel as written and carry the platform they were authored on; in a subscribed collection on the other OS their row shows "authored on macOS" or "authored on Windows".
- The same
cmd /crule applies to a connector arriving from a document, not just to one typed into the editor: Windows skips a remote connector whose Server URL, header name or OAuth client ID contains& | < > ^ "or a space, or whose OAuth scopes contain any of those but a space. The Import sheet lists it as skipped with the field at fault, and every later update from that document leaves it out. The same document imports whole on a Mac, which writes barenpx. - The same goes for the folder
${COLLECTION_DIR}stands for. On Windows a connector launched throughcmd /cthat uses the token shows a caution when that folder holds& | < > ^ "or a space: "The folder ${COLLECTION_DIR} stands for must not contain…". Folder names with spaces are ordinary on Windows, so a collection meant for PCs is best published into a folder without one. - An app older than collections, sharing the same master list, has no idea a collection is subscribed and edits it as an ordinary one. This app then reads those edits as a pending update from the source, and applying reverts them; use Make Local Copy or Stop Syncing first if you want to keep them.
- collections.json travels with mcps.json. The per-machine facts — where a source document sits, which folder a collection publishes to — live in a collections-local.json that stays out of the synced folder, as backups do.
Requires Xcode 15.4+ (Swift 5.10). Command Line Tools alone can compile the
app but cannot run the test suite; if xcode-select points at them, set
DEVELOPER_DIR to Xcode's Contents/Developer for swift test.
git clone https://github.com/dlaporte/connector-control.git
cd connector-control
swift test # the full suite, no network, never touches your real config
./scripts/build-app.sh # → build/Connector Control.app (ad-hoc signed)
cp -R "build/Connector Control.app" /Applications/
For development against a throwaway config instead of your real one:
mkdir -p .sandbox/store
cp "$HOME/Library/Application Support/Claude/claude_desktop_config.json" .sandbox/
CONNECTOR_CONTROL_CLAUDE_CONFIG="$PWD/.sandbox/claude_desktop_config.json" \
CONNECTOR_CONTROL_STORE_DIR="$PWD/.sandbox/store" \
swift run ConnectorControl
The two overrides cover Claude's config and the store folder (the master list, collections.json and backups). This machine's collections-local.json is still the real one, and a sandbox run rewrites its record of which collection Claude's config came from, so quit the installed app first.
Requires the .NET SDK 10.0.400 or a later 10.0.x (windows/global.json).
The solution also builds — but the app cannot run — on a Mac or Linux with
the same SDK, which is how the shared Core tests run on both.
dotnet test windows/ConnectorControl.slnx # Core + app tests, no network
dotnet run --project windows/src/ConnectorControl.App
The same CONNECTOR_CONTROL_CLAUDE_CONFIG and CONNECTOR_CONTROL_STORE_DIR
overrides point a development run at a throwaway config; collections-local.json,
settings.json and crash.log stay in %LOCALAPPDATA%\Connector Control, and
while the installed app is running a second copy only opens its flyout, so
quit it first. Installers are built by windows/scripts/package.ps1
(Velopack, vpk at the version pinned in windows/.config/dotnet-tools.json,
which must match the Velopack package in ConnectorControl.App.csproj; the
Windows build fails otherwise), which is also what the in-app updater
consumes.
Releases are produced by .github/workflows/release.yml
on an exact vX.Y.Z tag on master — both platforms from one tag, onto one GitHub release: the
universal Mac build, Developer ID signing with hardened runtime, Apple
notarization of both the app and the DMG, stapling, and the Sparkle
appcast; and the Windows installers for x64 and Arm64, code-signed with
Azure Artifact Signing and checked on a Windows runner by a silent install of
the x64 package and a signature check of the Arm64 one. Actions ▸ Release ▸
Run workflow rehearses a version end to end and publishes nothing.
Preview builds (both apps): push a preview-<n> tag, or run Actions ▸ Preview ▸
Run workflow with a number (a dry run by default). A preview builds <next>-preview.<n>,
where <next> is the top ## vX.Y.Z heading of CHANGELOG.md, signs and notarizes the
Mac app and signs the Windows installers exactly like a release, and publishes them as
one GitHub prerelease. Stable users are unaffected: a prerelease is never
releases/latest, so the Mac update feed does not change. A Windows preview install
updates itself to later previews and to the final release; a Mac preview is offered the
final release when it ships, and each new preview is downloaded by hand. A preview-dry-<n> tag builds everything and publishes nothing.
The Mac job runs in the signing environment, whose deployment branch policy must allow
preview-* tags and any branch previews are cut from.
The release, preview and Windows CI workflows all call one Windows build definition,
windows-build.yml; the release and
preview workflows call one Mac signing build,
mac-build.yml, which alone runs in the
signing environment; and every workflow's own YAML and shell/PowerShell
scripts are linted by infra-ci.yml.
| Script | What it does | Who calls it |
|---|---|---|
scripts/build-app.sh |
Assembles build/Connector Control.app from the SwiftPM build products, embedding Sparkle and the app icon. |
mac-ci.yml, mac-build.yml |
scripts/make-dmg.sh |
Packages the app bundle into a drag-to-Applications DMG. | mac-ci.yml, mac-build.yml |
scripts/test-mac.sh |
Runs the Swift suite the way CI gates it (no test may skip or fail). | mac-ci.yml, preview.yml, release.yml |
scripts/regenerate-goldens.sh |
Rewrites the golden JSON fixtures under Tests/Fixtures/ from the Swift Core's output after a deliberate format change; the C# tests compare against the same files. |
run by hand, on a Mac |
scripts/generate-icon.swift |
Renders the app icon — macOS .icns or Windows .ico, chosen by the output extension. |
scripts/build-app.sh; the .ico path is run by hand, on a Mac |
scripts/mac/import-signing-cert.sh |
Imports the Developer ID certificate into a throwaway CI keychain. | mac-build.yml |
scripts/mac/notarize.sh |
Submits a binary or app bundle for Apple notarization and staples the ticket. | mac-build.yml |
scripts/mac/make-appcast.sh |
Builds and EdDSA-signs the Sparkle appcast for one release. | mac-build.yml, for release.yml only |
scripts/release/changelog-section.sh |
Prints one version's CHANGELOG.md section. | release.yml, scripts/release/preview-notes.sh |
scripts/release/preview-notes.sh |
Prints the release notes for a joint preview build. | preview.yml |
scripts/release/ensure-release.sh |
Creates a GitHub release, or reuses one a previous run already created. | release.yml, preview.yml |
scripts/release/upload-release-assets.sh |
Uploads one build's Velopack assets to an existing release. | release.yml, preview.yml |
scripts/release/verify-release.sh |
Verifies a release's draft/prerelease flags and asset set. | release.yml, preview.yml |
windows/scripts/package.ps1 |
Publishes and Velopack-packs one Windows runtime. | windows-build.yml |
windows/scripts/smoke-test.ps1 |
Installs a packed Setup.exe silently and proves the app starts and stays up (with -ExpectSigned, that it and its package are signed); with -SignatureOnly, only checks a Setup.exe's signature. |
windows-build.yml |
windows/tools/probe-claude.ps1 |
Manual diagnostic for how Claude Desktop installs and is found on a PC. | run by hand, on Windows |
- Manages the
mcpServerssection of Claude Desktop's config only — not claude.ai web connectors, Claude Desktop extensions, or Claude Code's MCP configuration. - Neither app is sandboxed: each needs to read and write Claude Desktop's config file and to quit and relaunch Claude.
- Restarting Claude interrupts any in-progress conversation; the app asks first by default (Settings ▸ General).
- Windows: Claude Desktop is found by its app package (
Claude_pzs8sxrjxfjjc) or, for older installs, its program folder; if Claude does not come back after a restart the app says so rather than guessing.
MIT — © 2026 David LaPorte







