8 min read

Remove abandoned MCP servers without leaving access behind

Remove abandoned MCP servers safely by finding every config and launcher, revoking live credentials, deleting old tools, and proving access is gone.

Remove abandoned MCP servers without leaving access behind

Abandoned MCP servers deserve the same treatment as abandoned deploy scripts: assume they still work until you prove that they do not. A stale entry can start a local command, direct an agent to a remote endpoint, expose an old environment variable, or preserve a route to a credential that nobody now owns.

I have seen developers delete one configuration block, call the job finished, and discover months later that the same tool still launches from an editor extension or a repository file. The fix is not a larger spreadsheet. You need to separate discovery, revocation, removal, and proof that the access path has stopped.

A removed configuration is not a revoked access path

Removing abandoned MCP servers means closing every route that lets an agent perform work, not merely hiding a server from one client menu. The server definition is only one part of that route.

A typical local setup has four pieces: a client configuration, a launcher command, configuration or environment passed to that command, and authority at the target service. A remote setup replaces the local command with a URL, but it still has client configuration and authority at the far end. Teams often remove the first piece and leave the rest intact.

The Model Context Protocol specification describes servers as providers of capabilities such as tools, resources, and prompts. It also supports different transports, including stdio and HTTP-based connections. That distinction matters during retirement. A stdio server may only run when a local client starts its command. A remote service can remain available after every developer deletes the local entry that pointed to it.

Do not blur these cases:

  • A configuration record tells a particular client where to find a server.
  • A launcher is the executable, script, container command, extension, or service that starts or reaches it.
  • An authority grant is the token, SSH identity, OAuth authorization, account session, or network permission that lets the action succeed.
  • An execution record shows that a client or agent actually used the path.

Getting this wrong creates two bad outcomes. You may leave an old tool able to reach production data, or you may revoke a credential that an active tool still needs and turn a routine cleanup into an incident.

Start with a retirement rule that has teeth: if a server lacks a current owner, a documented purpose, and evidence of recent intended use, disable it while you investigate. "We might need it someday" is not ownership. A repository can preserve the setup privately if someone later needs to rebuild it with a fresh credential.

Inventory the clients before searching the disk

Your first inventory should list clients, not servers, because every client reads configuration from different places. Developers commonly have more than one agent client installed, plus an editor integration, a terminal helper, and project-specific settings committed beside code.

Write down each place that can initiate an MCP connection on the machine. Include desktop applications, command-line agents, IDE extensions, local scripts that invoke an agent, and any remote development environment that mounts the home directory. Ask the developer what they actually use, then verify it. Memory is poor evidence when the tool ran once during a prototype six months ago.

For each client, record its version, configuration locations from its own documentation, and whether it supports user-level and project-level settings. Do not guess a path because another client used it. Configuration locations change, and an invented path gives people false confidence.

A useful inventory row has enough detail to make a retirement decision later:

FieldRecord
Client and configuration pathWhich program reads the file, and where it lives
Server nameThe label shown to the agent or user
Transport and launcherstdio command, URL, extension, container, or script
Owner and purposeA person responsible and the active work it supports
Target systemsAPIs, hosts, repositories, data stores, or local folders reached
Authority sourceToken store, environment variable, SSH identity, OAuth grant, or managed identity
DecisionKeep, replace, suspend, or retire

Project settings cause the most surprises. A developer can clean their home configuration and still launch a server whenever they open an old repository. Search the current branch, ignored files used for local setup, sample configuration files, and onboarding scripts. Also inspect shared dotfile repositories. A stale server often survives because somebody copied a convenient snippet into a template.

Do not treat the inventory as a compliance artifact. Use it to drive actions. If you cannot name the target system and the authority source for a server, mark it as unresolved and prevent casual use until you can.

Search for launchers, not just files named MCP

A text search for "mcp" finds obvious configuration, but launchers hide under generic script names and package bins. Search for the server label, command name, hostname, port, package name, and environment variable names that the inventory exposes.

On macOS, this command gives you a bounded starting list of likely JSON configuration files in a home directory. It intentionally skips the cache tree, which otherwise produces a pile of irrelevant package metadata.

find "$HOME" -type f \( -name '.mcp.json' -o -name 'mcp.json' -o -name '*mcp*.json' \) \
  -not -path "$HOME/Library/Caches/*" \
  -print 2>/dev/null

Expect output shaped like this:

/Users/dev/work/acme-api/.mcp.json
/Users/dev/Library/Application Support/example-client/settings.json
/Users/dev/.config/example-agent/mcp.json

The list is evidence, not a deletion list. Open each file and identify which client owns it. A filename with mcp may be documentation, an archived experiment, or a generated lock file. Conversely, a settings file with a bland name may contain the actual server definition.

For JSON files whose schema uses an mcpServers object, this command prints a compact review table. It does not modify files.

jq -r '
  .mcpServers // empty
  | to_entries[]?
  | [.key, (.value.command // .value.url // "unknown"),
     ((.value.args // []) | join(" "))]
  | @tsv
' path/to/config.json

A typical result looks like this:

issue-tracker	npx	-y @example/issues-mcp
legacy-reporting	https://reports.internal.example/mcp	

If the command produces nothing, do not infer safety. The file may use another schema, the client may store settings elsewhere, or the service may arrive through an extension.

Then inspect the launch surfaces that survive ordinary file cleanup. On a Mac, look at shell profiles, editor task settings, package-manager global binaries, and user launch agents. launchctl print gui/$(id -u) can reveal processes that launch under the logged-in user, but its output may expose command arguments or environment values. View it locally and do not paste it into a ticket or chat.

Search content with narrow terms rather than scanning and exporting the whole home directory. For example, once you know an abandoned server calls old-report, search for that literal name, its old hostname, and its executable. This approach finds wrappers such as scripts/agent-tools.sh without turning a cleanup review into a private-data collection exercise.

Classify each server by current authority

A server can look dead and still hold live authority, so classify it by how it authenticates before you touch its files. The same server name may use different credentials on different machines, which is why a team-wide removal needs per-machine evidence.

Use five practical categories. They describe where the power sits, not how the server markets itself.

  1. No remote authority. The server reads local, non-sensitive files or produces local output. It can still create a supply-chain or privacy problem, but revocation usually means removing its process permissions and configuration.
  2. Environment-backed authority. The launcher receives an API token, password, or connection string through a shell profile, .env file, IDE setting, or launch configuration.
  3. File-backed authority. The launcher reads an SSH private key, client certificate, service-account file, or local credential database.
  4. Provider-managed authority. The server uses OAuth, an application installation, a device login, or a managed identity. The provider, not a local text file, controls the grant.
  5. Network and account authority. The server needs no explicit secret because a corporate network, local account, VPN session, or allowlisted address lets it reach a service. This category is easy to miss and hard to retire cleanly.

Record the exact account, scope, and target for every authority source. "Git token" is not enough. You need to know whether it belongs to a personal account, a bot account, or a shared machine identity, and whether it can read repositories, write issues, trigger deployments, or access administrative APIs.

This is where cleanup exposes uncomfortable shortcuts. A local MCP command that receives a broad personal token through ~/.zshrc does not become harmless because the developer stopped using it. The token may also feed other scripts, so revoking it requires coordination. That is not a reason to postpone the work. It is a reason to map dependencies before the revoke.

Keep the inventory factual. Do not put token values, private keys, full authorization headers, or copied configuration contents into it. A reference such as "credential entry named reporting-read" or "SSH fingerprint ending in 3f:91" lets the right person find the authority without creating another secret repository.

Revoke at the service before deleting local evidence

Find sessions worth revoking
Review agent runs in the Sessions journal, then revoke a session immediately when an old workflow appears.

Revoke active authority at the provider or target service before you remove the local configuration. This order stops a copied configuration, another machine, or an unnoticed launcher from continuing to use the same grant.

For API tokens, use the provider's token management interface or its documented revocation endpoint. Confirm which token you are revoking by its identifier, label, account, creation details, or last-use record where the provider exposes one. Then remove its value from local files and credential stores. Never test a revoked token by pasting it into a web form or a shell command that might save history.

OAuth needs special care. RFC 7009 defines a token revocation request sent to the authorization server's revocation endpoint. It also permits the server to return a successful response even if the token is invalid, which prevents callers from learning whether a token exists. That response shape means a 200 status alone does not prove you revoked the intended grant. Check the provider's authorization or connected-app view, then make a controlled call through the old path only if your normal security practice permits it.

For SSH, remove the public key or deploy key from every place that accepts it. That may include an account's authorized keys, repository deploy-key settings, a bastion account, a CI service, and a configuration-management source that repopulates authorized_keys. Deleting ~/.ssh/old_agent_key only removes one local copy. It does nothing to another copy and nothing to the server-side authorization.

For application installations and service accounts, disable or delete the installation, rotate the client secret or private credential if exposure is plausible, and remove role bindings that existed only for the retired server. Treat broad roles as a cleanup item even if the service account stays. An agent tool rarely needs the same access as a human administrator.

Network-based access calls for a different conversation. Remove old allowlist entries, firewall rules, VPN group membership, or internal DNS routes only after you establish their owner and consumers. Do not use an MCP cleanup ticket as permission to break an unrelated integration. Instead, isolate the specific rule and set a deadline for its owner to confirm use.

Write down the revoke result as an event: who performed it, which authority identifier they revoked, where they performed it, and how they verified it. Avoid recording secrets or screenshots that contain them. This record matters when a repository later fails and someone asks whether the cleanup caused it.

Remove local definitions in a reversible sequence

Remove local definitions after upstream revocation, using a sequence that preserves a private rollback path without preserving live credentials. A careless uninstall can leave a project unable to explain what it relied on. An overcautious archive can keep a usable token in a forgotten folder. Split configuration from secret material.

Use this sequence for one server at a time:

  1. Stop the relevant agent client and editor extension. A running client may keep a child process alive or rewrite its settings on exit.
  2. Copy the server's non-secret configuration fields into the retirement record: name, command or URL, arguments, expected target, owner, and removal date. Replace secret values with a description of where they lived.
  3. Revoke the authority at the target service and record the confirmation details.
  4. Remove the server entry from every identified user-level and project-level configuration. Delete environment variables and references to retired credential files.
  5. Uninstall the dedicated package, extension, container image, or wrapper script if no active server uses it. If other work uses the package, remove only the abandoned command and document the shared dependency.

Avoid editing with a broad search-and-replace. JSON commas, shell quoting, and shared environment blocks punish casual edits. Use the client's settings interface when it writes valid configuration predictably. Otherwise, make a backup with restrictive file permissions, edit one object, and validate the result before reopening the client.

For JSON, jq gives a simple syntax check:

jq empty path/to/config.json && echo "valid JSON"

That proves the JSON parses. It does not prove the client accepts the schema or that you removed every reference. Read the relevant object after the edit, then use the client to inspect its configured server list.

Do not archive a full .env, a private key, or a settings file containing a bearer token in a project archive directory. Version control, cloud backup, and desktop search make that mistake travel far. Preserve a redacted record and rely on the provider's audit history for the fact that the former credential existed.

Package removal also needs restraint. A globally installed runtime package may support several active tools. Before deleting it, ask which executable name each live configuration invokes. I have watched a cleanup remove a shared runtime dependency, then force an unrelated team to debug a broken agent session because the error message mentioned a missing package rather than the deleted tool.

Prove that nothing starts or reaches the retired server

Require approval for old credentials
Mark sensitive keys for approval on every use instead of leaving a broad token available to an agent.

A retirement is complete when the relevant clients cannot discover, launch, or authenticate to the server. Configuration review alone proves none of those things.

Restart the client completely. Closing a window may not stop a menu-bar helper, editor host, or child process. Reopen the client and inspect its configured server list using its normal diagnostics. If it displays the retired server, you missed a configuration source or a synchronization mechanism restored it.

Next, perform a narrow launch test. Open the repository that formerly supplied the server configuration, start the agent, and ask for a harmless action unrelated to the retired server. Watch local process activity for the retired executable name and inspect the client's logs for connection attempts to the former hostname. Do not invoke the retired tool against a live production target merely to see whether it fails.

For remote endpoints, use provider audit records, access logs, or account activity to check for attempted use after the revoke. A denied request after a controlled test proves the authority no longer works. Silence in a log is weaker evidence because the client may never have attempted the connection.

Also check that the old credential no longer appears where it should not. Search for the token's label, environment variable name, known hostname, file name, and SSH public fingerprint. Do not search for a full secret value if it could end up in shell history, terminal scrollback, or a command log. The label and reference point are usually enough.

A failure worth recognizing looks like this: a developer removes legacy-reporting from a personal agent configuration, restarts the agent, and sees nothing. A week later they open an older repository. Its local settings run npx with an old package, the script loads REPORTING_TOKEN from a shell profile, and the remote API still accepts the token. Each individual check looked clean because each checked only the personal configuration. The inventory should have joined repository configuration, launcher, environment source, and API grant before removal began.

Preserve evidence without creating another secret cache

Verify retirement evidence offline
Verify Sallyport's hash-chained audit log offline over ciphertext with sp audit verify, without unlocking the vault.

Keep enough evidence to explain a retirement, but do not convert your audit folder into an archive of usable access. The record should let another engineer answer what existed, what it could reach, who approved removal, and what proof closed it.

A compact retirement record can include the server identifier, local locations removed, command or endpoint without credentials, target service, authority type, grant identifier or fingerprint, revocation date, owner, and validation result. Put access-controlled operational notes where your team already handles security records. Do not create a new shared document full of copied settings.

Execution history can expose dependencies that inventory misses. Look for agent session history, client logs, package-manager install history, source-control changes that added configuration, and target-service activity. Treat timestamps carefully. A log can show that a process ran, but it may not prove a tool completed a privileged action.

Sallyport records agent runs and individual HTTP or SSH calls in separate journals projected from a write-blind encrypted audit log. If a team uses it, sp audit verify can verify that audit chain offline over ciphertext without needing the vault key, which helps preserve evidence during an access review.

Do not confuse tamper evidence with a complete inventory. A log only records actions that passed through the logging point. It will not reveal an old server that ran outside it, a forgotten configuration that nobody launched, or a copied token used directly by another script.

Make ownership expire before tools become archaeological finds

The least painful cleanup happens when teams assign an owner and review date at installation time. That rule sounds administrative until you have to identify why an agent can still reach an API whose original project ended years ago.

Ask for a short retirement plan whenever somebody adds a server with write access, production visibility, or broad repository reach. The plan should state the owner, intended repositories, authority type, target systems, and the event that triggers removal. A prototype can have a short review date. A shared tool can have a named maintainer. Neither should have an unnamed permanent exception.

Use repository configuration sparingly. A project-level server is appropriate when the project truly requires it and its setup has no credentials embedded in the file. It is a poor place for a developer's personal experimentation. Keep experiments in a private, disposable location, then either promote them with ownership or remove them before the worktree becomes a template.

Review after agent client changes, team handoffs, repository archival, and credential rotations. Those events expose drift more reliably than an arbitrary calendar ritual. When the review finds an unknown server, suspend its route to sensitive systems first, then trace its owner and dependencies. A tool that nobody can explain should not retain permission to act.

The next cleanup should begin with one real machine and one active client. Build the inventory until every server has an owner, a launch path, and an authority source. The entries that cannot meet that standard have already told you what to retire.

FAQ

Is an unused MCP server a security risk?

No. An entry can be dormant while its command, remote URL, environment variables, and credentials remain ready for the next client that reads it. Remove the configuration and revoke the credentials separately.

What is the difference between a local and remote MCP server?

A server that starts through stdio runs on the developer machine when the client launches it. A remote server may remain reachable independently of any local configuration, so deleting the local entry does not close its remote access.

Where are MCP server configurations stored on macOS?

Search each client’s documented settings, project repositories, shell startup files, editor settings, launch agents, and package-manager bin directories. A home-directory text search helps, but it will not find every managed or generated configuration.

Can I delete an MCP configuration without breaking my projects?

Yes, if you remove them in the right order: identify the owner, revoke the upstream credential, archive the configuration privately, remove the launcher, then restart the client. Deleting a JSON block first is fast but leaves too much uncertainty.

Should I remove old API tokens from environment variables?

Treat an old environment variable as an access path until you prove otherwise. Remove it from shell profiles, project env files, launch configurations, and CI settings after you revoke the related token.

How do I revoke SSH access for a retired tool?

Remove the exact public key, deploy key, machine user, or certificate path that granted access. Deleting a local private key alone does not stop a copied key or another credential from reaching the same account.

How can I confirm an MCP server is really gone?

Run the client with a clean profile or inspect its server list after a full restart. Then search its logs or activity records for the retired command and verify that no shell process, launch agent, or editor extension starts it.

How often should teams review MCP servers?

Keep a dated inventory with the server name, launch method, owner, credential location, systems reached, and retirement decision. Review it whenever a developer changes agents, editors, or a shared repository template.

What should I do before uninstalling an MCP server package?

Do not treat configuration deletion as credential revocation. If the server used OAuth, revoke the token with the provider; if it used an API token or SSH key, remove that grant at the service before deleting local files.

Can an action gateway help manage agent access?

Sallyport can keep API and SSH credentials out of the agent process and record agent sessions and individual calls. It does not replace inventory work, because you still need to remove agent configurations and retire access that no longer has an owner.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov