How to restrict which MCP servers Claude Code can use

To restrict which MCP servers Claude Code can use, deploy a managed-mcp.json file to fix the server set, or put allowedMcpServers and deniedMcpServers in managed settings to filter what users add. Denylist matches always win. Set allowManagedMcpServersOnly to true so a developer cannot broaden the allowlist in their own settings.
Choose a pattern
Claude Code supports several restriction levels, and each one uses a different key. Pick the row that matches how much control you need. The docs describe these patterns directly, so the table below is a summary, not a new policy model.
| Pattern | What it does | Configure |
|---|---|---|
| Fixed deployment | Every user gets the same servers and cannot add others | managed-mcp.json with the servers you want |
| Disable MCP | No servers load except the few that load under exclusive control | managed-mcp.json with an empty mcpServers map |
| Provided servers | Every user gets the remote servers you list and keeps their own | managedMcpServers in managed settings |
| Approved catalog | Users add the approved servers, and anything else is blocked | allowedMcpServers with allowManagedMcpServersOnly: true |
| Soft allowlist | An allowlist that users can broaden in their own settings | allowedMcpServers without allowManagedMcpServersOnly |
| Denylist only | Block known-bad servers and allow everything else | deniedMcpServers |
Deploy a fixed server set with managed-mcp.json
When managed-mcp.json is present, Claude Code loads only the servers it defines, plus any you provide through managedMcpServers and a few built-in ones. Users cannot add, change or use other servers, including plugin servers and servers passed with --mcp-config.
- Write
managed-mcp.jsonwith anmcpServersobject. Use the same format as a project.mcp.jsonfile. - Save the file at the system path for each operating system (see the table below).
- Deploy it with your device management tool. The file needs administrator rights to write, so MDM, Group Policy or your fleet tool is the usual route.
- Confirm the policy is active with the checks below.
| Platform | Path |
|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-mcp.json |
| Linux and WSL | /etc/claude-code/managed-mcp.json |
| Windows | C:\Program Files\ClaudeCode\managed-mcp.json |
{
"mcpServers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/"
},
"sentry": {
"type": "http",
"url": "https://mcp.sentry.dev/mcp"
}
}
}
Do not put API keys in an env block in this file. Any user on the machine can read it. The docs point to ${VAR} expansion, OAuth or headersHelper for per-user credentials.
A user’s own servers stop loading once the file is in place. Claude Code gives no warning that policy is the reason, so tell developers before you roll it out. Setting "mcpServers": {} disables MCP for everyone. Servers you provide through managedMcpServers, and the built-in ones the docs list, still load under an empty map.
Filter with allowlists and denylists
Allowlists and denylists filter servers that a user, a plugin or your organization has already configured. They are not a registry. Claude Code checks each server in three steps, in this order:
- Merge the lists from every settings scope into one allowlist and one denylist. If
allowManagedMcpServersOnlyistrue, only the managed allowlist is kept. - Check the denylist. A match blocks the server, and nothing overrides it.
- Check the allowlist. If
allowedMcpServersis not set anywhere, every server that passed step 2 loads. If it is set, a remote server must match aserverUrlentry and a stdio server must match aserverCommandentry.
Leaving allowedMcpServers unset is not the same as an empty array. Unset allows every server. An empty array [] allows none, apart from the servers that skip the check. deniedMcpServers behaves the same whether it is unset or empty, because an empty denylist blocks nothing.
Match servers by URL, command or name
Each entry has one key. Pick the key that identifies what actually runs.
| Key | Matches | Use it for |
|---|---|---|
serverUrl |
A remote URL, exact or with * wildcards |
HTTP and SSE servers |
serverCommand |
The exact command and arguments, as one array | Stdio servers |
serverName |
The label the user assigned. Exact match only | Denylist entries, or claude.ai connectors by display name |
The docs call serverName “not a security control”, because a user can name any server github. A serverCommand entry matches every argument in order, and it does not compare the env block. A serverUrl pattern with no port matches only the scheme’s default port, so https://mcp.example.com/* does not match https://mcp.example.com:8443/.
Example: an allowlist with a denylist
The example below comes from the docs. It allows two remote servers and two stdio commands. It blocks one name, one command and one host pattern, whatever the allowlist says.
{
"allowedMcpServers": [
{ "serverUrl": "https://api.githubcopilot.com/*" },
{ "serverUrl": "https://mcp.sentry.dev/*" },
{ "serverCommand": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "."] },
{ "serverCommand": ["python", "/usr/local/bin/approved-server.py"] }
],
"deniedMcpServers": [
{ "serverName": "dangerous-server" },
{ "serverCommand": ["npx", "-y", "unapproved-package"] },
{ "serverUrl": "https://*.untrusted.example.com/*" }
]
}
Once one serverUrl entry exists, every remote server must match a URL pattern. A user cannot slip an unlisted remote server through by giving it an allowed name.
Lock the allowlist to managed settings
Without a lock, allowlists from every settings scope merge, including a developer’s own ~/.claude/settings.json. That means a developer can add an entry that your managed list does not contain. To stop that, set both keys in a managed settings source.
- Open the managed settings file for the platform (
managed-settings.jsonin the same system directory asmanaged-mcp.json). - Add
"allowManagedMcpServersOnly": trueand yourallowedMcpServerslist. - Deploy the file, then check the result with
/status.
{
"allowManagedMcpServersOnly": true,
"allowedMcpServers": [
{ "serverUrl": "https://api.githubcopilot.com/*" },
{ "serverUrl": "https://*.internal.example.com/*" }
]
}
With the lock on, the denylist still merges from every scope. Developers can always block a server for themselves. allowManagedMcpServersOnly is separate from allowManagedPermissionRulesOnly, which locks permission rules but does not enforce the MCP allowlist.
Watch for the gaps
Most of the surprises come from three rules in the docs.
| Gap | What happens | Fix |
|---|---|---|
serverName used as a control |
A user renames a server to match the entry | Match serverCommand or serverUrl instead |
| Managed servers skip the allowlist | Entries in managed-mcp.json load without an allowlist match, unless they use ${VAR} outside env |
Add a deniedMcpServers entry for any managed server you want blocked |
| Upgrade with no notice | managed-mcp.json servers you kept off only through an allowlist can start loading on a client version that does not check them |
Add denylist entries before users upgrade |
--mcp-config with a managed file |
On a workstation, Claude Code exits at startup with You cannot dynamically configure MCP servers when an enterprise MCP config is present |
Remove the flag from scripts that run on managed machines |
The version gate matters for the upgrade row. The docs say that before v2.1.305, a managed server that used ${VAR} only in env was also checked, and before v2.1.259 every managed server was checked whenever an allowlist was set. Check your floor version before you rely on the allowlist for managed servers.
Check that the policy is active
Run these checks on a managed machine. The docs describe both results.
- Run
claude mcp list. It should show only the servers inmanaged-mcp.json, plus any you provide throughmanagedMcpServers. If a user’s own servers still appear, Claude Code is not reading the file. - Run
claude mcp add --transport http test https://example.com/mcp. Under exclusive control it fails withCannot add MCP server: enterprise MCP configuration is active and has exclusive control over MCP servers. The URL does not need to be real, because the check runs first. - Run
/statusin Claude Code to see which managed source applies.
Test denylist changes with a harmless server. A blocked server disappears from /mcp and from claude mcp list without a message, so the list is the only signal you get.
Enforcing this with AIControls
Settings files control one machine at a time. AIControls supports Claude Code through its gateway for both the CLI and VS Code, and it provides per-user tokens and per-user and per-session cost and budget limits. Those controls cover model traffic. They do not replace the server lists above, so keep both in place. Download AIControls to run it in your own cluster.
Next steps
Start with the denylist, because it blocks known-bad servers without changing what your developers can do. Move to an allowlist with allowManagedMcpServersOnly once you have an approved catalog. Use managed-mcp.json only where every developer should get the same servers. For how these controls fit into wider agent governance, see MCP security and governance for AI coding agents. For the identity side of MCP access, see Okta cross app access for AI agents, and for a buyer’s view of gateways, read the AI gateway buyer’s guide.
Frequently asked questions
What is the difference between managed-mcp.json and allowedMcpServers in Claude Code?
managed-mcp.json is a fixed server set. When it is deployed, Claude Code loads only the servers it defines, and users cannot add others. allowedMcpServers is a filter. It lets users add servers, but only those that match an allowlist entry. Use the file when every developer should get the same servers, and the lists when you want to approve a catalog and let people choose from it.
Does a denylist entry override an allowlist entry in Claude Code?
Yes. Claude Code checks the denylist before the allowlist, and nothing overrides a denylist match. A server that matches an allowlist URL pattern is still blocked if it also matches a deniedMcpServers entry. Denylist entries merge from every settings scope, so a user can also block a server for themselves.
Why does a serverName entry not stop a developer from running a server?
The serverName value is only the label a user assigns when adding a server. A user can call any server github, so the name does not identify what runs. For enforcement, match on serverCommand for stdio servers or serverUrl for remote servers. Use serverName only for a denylist entry where a label is enough.
Can a developer add an MCP server that the managed allowlist does not list?
Not when the allowlist is locked. Set allowManagedMcpServersOnly to true in a managed settings source, and Claude Code ignores allowlists in user, project and local settings. Without that flag, a developer's own allowlist entries merge with yours and can broaden what loads.