Skip to content

Managed MCP

Control which MCP servers your organisation's developers can use, from a fixed approved set to provided servers, allowlists, denylists and switching MCP off.

Out of the box, anyone running Claude Code can connect whatever MCP server they like. Anthropic reviews connectors before listing them in its directory, but it does not security-audit or operate MCP servers. If you are responsible for what runs on your developers' machines, you will probably want some control here.

Claude Code gives administrators three tools, which combine into several patterns:

  • managed-mcp.json: a file that takes exclusive control and defines the only servers allowed.
  • managedMcpServers: a managed setting that adds remote servers for everyone while leaving their own in place.
  • allowedMcpServers / deniedMcpServers: filters applied to whatever servers people configure.

These cover servers Claude Code loads itself, including claude.ai connectors it fetches. Connectors the desktop app injects into its local and SSH sessions are governed from your claude.ai organisation settings instead.

Picking a pattern

PatternEffectHow
MCP offNothing loads except the few servers allowed under exclusive controlmanaged-mcp.json with an empty mcpServers map
Fixed setEveryone gets the same servers and cannot add othersmanaged-mcp.json listing them
Provided serversEveryone gets your remote servers and keeps their ownmanagedMcpServers
Approved cataloguePeople add from your list; anything else is refusedallowedMcpServers plus allowManagedMcpServersOnly: true
Plugins onlyNo servers from ~/.claude.json or .mcp.json; plugin servers still loadstrictPluginOnlyCustomization including mcp
Soft allowlistYour allowlist, which users can extendallowedMcpServers alone
DenylistBlock known-bad servers, allow the restdeniedMcpServers
OpenAnything goesDeploy nothing

There is no built-in browsable registry. For the catalogue pattern, publish the approved list with its claude mcp add commands on your wiki, or package the servers as plugins in a managed marketplace so people can install them from /plugin.

Exclusive control with managed-mcp.json

Once a managed-mcp.json file is on the machine, Claude Code loads only:

  • servers defined in the file;
  • servers you provide via managedMcpServers;
  • in-process servers registered by the host app (the VS Code extension's own server, connectors the desktop app delivers);
  • the built-in Claude in Chrome server, if you explicitly allow it.

Everything else is off: user-added servers, plugin servers, servers passed via --mcp-config, and (unless you allow them) claude.ai connectors.

Deploying the file

This is a standalone file, so it cannot travel through server-managed settings. Push it with whatever tooling writes to system paths: Jamf or a configuration profile on macOS, Intune or Group Policy on Windows, your configuration management on Linux.

PlatformPath
macOS/Library/Application Support/ClaudeCode/managed-mcp.json
Linux and WSL/etc/claude-code/managed-mcp.json
WindowsC:\Program Files\ClaudeCode\managed-mcp.json

The format is the same as a project .mcp.json. A plausible set for an agency that uses Linear, an internal docs search and a local database inspector:

{
  "mcpServers": {
    "linear": {
      "type": "http",
      "url": "https://mcp.linear.app/mcp"
    },
    "docs-search": {
      "type": "http",
      "url": "https://docs-mcp.agency.internal/mcp"
    },
    "db-inspect": {
      "type": "stdio",
      "command": "/opt/agency/bin/db-inspect-mcp",
      "args": ["--read-only"],
      "env": { "DB_INSPECT_PROFILE": "${DB_INSPECT_PROFILE}" }
    }
  }
}

Warning: Every user on the machine can read this file. Never put API keys in env. Use ${VAR} expansion from each user's environment, OAuth or per-user headers so people authenticate as themselves, or a headersHelper that produces credentials on connect.

The --mcp-config and --strict-mcp-config flags

With a readable managed-mcp.json in place:

  • On a workstation, passing --mcp-config stops Claude Code at startup with You cannot dynamically configure MCP servers when an enterprise MCP config is present.
  • In a cloud session on a host with the file (a self-hosted runner, for instance), the session starts with only the managed servers and silently drops connectors and other servers the host passed through --mcp-config. A warning naming them goes to stderr, which self-hosted runners log at debug.
  • --strict-mcp-config asks to replace the managed set, so it fails at startup everywhere.

Lists and the managed set

deniedMcpServers still filters the file's servers, and because denylists merge from every scope, a user can block a managed server for themselves.

allowedMcpServers does not apply to the file's servers, with one exception: a server whose definition uses ${VAR} expansion is still checked, since its real configuration depends on each user's environment. Before v2.1.259 every managed server had to pass the allowlist.

Note: If you previously relied on the allowlist to keep some of your own managed-mcp.json servers from loading, they start loading silently from v2.1.259 (unless they use ${VAR}). Add denylist entries or split the file per group before people upgrade.

Checking it works

  1. claude mcp list should show only the file's servers plus any managedMcpServers. If the user's own servers still show, Claude Code is not reading the file: check the path and directory permissions. If the file's servers are missing and the MCP config diagnostics section reports the enterprise config failed to parse, fix the error it names and restart.
  2. claude mcp add --transport http probe https://example.invalid/mcp should fail with Cannot add MCP server: enterprise MCP configuration is active and has exclusive control over MCP servers. The policy check happens before any connection, so the URL need not exist.

Switching MCP off

{ "mcpServers": {} }

claude mcp add then fails with the error above, and previously configured servers simply stop loading at the next session, with no message explaining why. Provided servers and anything you allow alongside still load, so leave those keys unset if you want MCP completely off.

Letting claude.ai connectors through

By default the file suppresses claude.ai connectors that Claude Code fetches itself, including ones an admin set up for the organisation. Set "allowAllClaudeAiMcps": true in an admin-controlled managed source (server-managed, MDM/HKLM, or system managed-settings.json) to load them as normal. Lists still apply, so you can deny individual ones. Plugin servers stay suppressed. A file on a cloud session's host suppresses that session's connectors regardless, and nothing in the file affects connectors the desktop app delivers to local or SSH sessions.

Letting Claude in Chrome through

With the file present, the built-in Chrome server is blocked in terminal sessions: no install prompt, no warning for users who enabled Chrome by default, and claude --chrome exits with an error naming the setting to change. From v2.1.282, set "allowClaudeInChromeWithManagedMcp": true in the device's own managed source (MDM profile, HKLM, or system file, whichever is selected). It is never read from server-managed settings. A deniedMcpServers entry for claude-in-chrome still wins.

Providing servers with managedMcpServers

From v2.1.259, managedMcpServers hands everyone a set of remote servers while they keep their own. Put it in any managed source: server-managed settings, a Claude apps gateway policy, an MDM profile or registry policy, or managed-settings.json. Older clients ignore it.

Entries look like HTTP or SSE servers in .mcp.json, including optional headers and oauth:

{
  "managedMcpServers": {
    "wiki": {
      "type": "http",
      "url": "https://wiki-mcp.agency.example/mcp"
    },
    "status-board": {
      "type": "sse",
      "url": "https://status.agency.example/mcp/sse",
      "headers": { "X-Board-Token": "shared-read-only-token" }
    }
  }
}

Anyone who can read managed settings on the machine can read those headers, so only use shared, low-privilege credentials, or rely on per-user OAuth.

Validation rules

An entry is dropped (with a notice in /status) unless:

  • type is http, sse, or the alias streamable-http;
  • url uses https:// (plain http:// is refused, even to localhost);
  • there is no command, args, env or headersHelper, so policy can never name a program to run;
  • no value contains ${VAR} (nothing is expanded);
  • the name uses only letters, digits, hyphens and underscores, and nothing contains control or invisible characters.

Claude Desktop has a setting of the same name that takes an array of a different shape. Do not copy one into the other; Claude Code rejects the array form.

How provided servers interact with everything else

  • A provided server beats a same-named server in local, project or user scope, and a plugin server or connector at the same URL.
  • With managed-mcp.json also deployed, both sets load and the file wins name clashes.
  • They keep loading when strictPluginOnlyCustomization locks mcp.
  • They need no allowlist entry, but the denylist (including a user's own) applies.
  • Without managed-mcp.json, --mcp-config can override a provided server of the same name for one run (subject to the allowlist), and --strict-mcp-config leaves provided servers out.

Users cannot remove them (claude mcp remove says the server is provided by the organisation), and a same-named entry they add is saved but ignored. They can turn one off for themselves in /mcp, where provided servers appear under Managed MCPs. /mcp and claude mcp get show only the host of the URL and header names without values.

Which sources count

The key is read from the selected managed source, or from every admin source under managedSourcesBehavior: "merge" (higher source wins name clashes). It is ignored, with a warning, in HKCU, host-supplied parent settings, and user, project or local files. It is not read in the desktop app's Code tab on third-party deployments or in Cowork, because Desktop controls MCP for those sessions.

Timing

Delivered through server-managed settings, a cached copy waits for the server to confirm it (up to 30 seconds) before MCP servers connect. If confirmation fails the session continues without them and /status says so. On a first launch with no cache, interactive sessions connect the servers as soon as they arrive, but a -p run might finish first. Gateway sign-ins load policy before the session starts, so neither delay applies.

Running interactive sessions pick up additions, changes (with a reconnect) and removals as soon as the new settings arrive. A -p run keeps a removed server until it ends.

Allowlists and denylists

These filter servers that already exist in someone's configuration; they do not create any. The denylist applies to everything except in-process type: "sdk" servers. Both lists also apply to servers passed with --mcp-config, and --strict-mcp-config does not bypass them.

On their own, allowlists from every scope merge, including the user's ~/.claude/settings.json, so users can widen what you allow. To make your list authoritative, set allowManagedMcpServersOnly: true alongside it in a managed source. From v2.1.273 that lock applies from any admin source, and the allowlist comes from the highest admin source that defines one. Denylists always merge from every scope.

Note: allowManagedMcpServersOnly is not the same as allowManagedPermissionRulesOnly. The latter governs permission rules and does nothing for MCP.

Entry types

Each list entry is an object with exactly one key:

KeyMatchesBest for
serverUrlThe server URL, exact or with * wildcardsRemote (HTTP, SSE) servers
serverCommandThe exact command and argument arrayStdio servers
serverNameThe user-chosen label, exact onlyConvenience, not security

Unset versus empty matters:

Unset[]Populated
allowedMcpServersEverything allowedNothing allowed (except servers that skip the check)Only matches allowed (same exception)
deniedMcpServersNothing blockedNothing blockedMatches blocked

serverName is whatever the user typed when adding the server, so anyone can call anything github. For claude.ai connectors it is the display name, which can change. Use it in the denylist for quick blocks of connectors (any non-empty string, for example { "serverName": "claude.ai Slack" }), but prefer serverUrl when renames or (N) suffixes would matter. In the allowlist, names are limited to letters, digits, hyphens and underscores. To kill every fetched connector at once, use disableClaudeAiConnectors.

serverCommand must match every argument in order. ["uvx", "acme-mcp"] does not match ["uvx", "acme-mcp", "--debug"]. The env block is not compared, and some environment variables change what an interpreter loads, so if you need to control env, define the server in managed-mcp.json.

serverUrl supports * anywhere, including the scheme. Host matching ignores case and a trailing dot; paths are case-sensitive. Without an explicit port, a fully written hostname matches only the scheme's default port, while a hostname containing * matches any port:

PatternMatches
https://tools.acme.dev/*Any path on that host, port 443 only
https://tools.acme.devSame (no path means any path)
https://tools.acme.dev:9443/*Port 9443 only
https://tools.acme.dev:*/*Any port, including 443
https://*.acme.dev/*Any subdomain, any port
http://localhost:*/*Any localhost port
*://tools.acme.dev/*Any scheme, each on its default port

The port rule applies to denials too. To block qa.acme.dev on every port and scheme, write *://qa.acme.dev:*/*; https://qa.acme.dev/* would miss a server on port 8443.

Variables in entries

From v2.1.219, serverCommand and serverUrl values in both the policy and the server config go through ${VAR} / ${VAR:-default} expansion before matching. serverName never expands. The server side expands from the live environment; the policy side expands from a pinned environment (the startup environment plus managed env), so a project file cannot redefine what your allowlist means. For allowlists, an expansion that would change a URL's scheme, host or path scope causes the entry to be ignored. For denylists, a variable missing at startup may be filled from user or managed settings, which only ever widens the match. Use literal values for anything you depend on, and on Windows reference variables that exist there (${USERPROFILE}, not ${HOME}).

Evaluation order

Every time a server would load, reconnect or be re-enabled in /mcp:

  1. Merge lists. Combine allowlists and denylists across scopes. With allowManagedMcpServersOnly, keep only the managed allowlist.
  2. Denylist. Any match by URL, command or name blocks the server. Nothing overrides this.
  3. Allowlist. If none is set, the server loads. Otherwise a remote server must match a serverUrl entry (a name match counts only if there are no URL entries), and a stdio server must match a serverCommand entry (a name match counts only if there are no command entries).

These skip the allowlist step: managedMcpServers entries, managed-mcp.json entries without ${VAR}, built-in servers (Claude in Chrome, the IDE integration server, CLI-configured servers), and the Slack tools a Claude Tag session uses. In-process sdk servers skip all three steps.

A worked policy

{
  "allowManagedMcpServersOnly": true,
  "allowedMcpServers": [
    { "serverUrl": "https://mcp.linear.app/*" },
    { "serverUrl": "https://*.agency.internal/*" },
    { "serverCommand": ["uvx", "mcp-server-git", "--repository", "."] }
  ],
  "deniedMcpServers": [
    { "serverUrl": "https://sandbox.agency.internal:*/*" },
    { "serverName": "claude.ai Gmail" }
  ]
}

What happens to various servers under this policy:

ServerOutcomeWhy
HTTP https://mcp.linear.app/mcpLoadsMatches a URL entry
HTTP https://wiki.agency.internal/mcpLoadsMatches the wildcard subdomain
HTTP https://sandbox.agency.internal:7000/mcpBlockedDenylist wins
HTTP named linear at https://evil.example/mcpBlockedURL entries exist, so the name is irrelevant
Stdio uvx mcp-server-git --repository .LoadsExact command match
Stdio uvx mcp-server-gitBlockedArguments differ
A user's own allowlist entry for anything elseIgnoredThe managed-only lock is on

What users see

SituationMessage or effect
claude mcp add with managed-mcp.json presentCannot add MCP server: enterprise MCP configuration is active and has exclusive control over MCP servers
claude --chrome with the file present and Chrome not allowedExit with Claude in Chrome is blocked by your organization's managed MCP configuration (managed-mcp.json). An administrator can allow it with allowClaudeInChromeWithManagedMcp in device policy.
Adding a denied serverCannot add MCP server "<name>": server is explicitly blocked by enterprise policy
Adding a server not on the allowlistCannot add MCP server "<name>": not allowed by enterprise policy
Adding a server under plugins-onlyCannot add MCP server: your organization's managed settings allow only MCP servers that plugins provide
Removing a provided serverMCP server "<name>" is provided by your organization (managed settings) and cannot be removed locally.
A configured server becomes blockedIt quietly disappears from /mcp and claude mcp list
Reconnecting a server blocked mid-sessionMCP server <name> is blocked by enterprise managed policy

Because blocked servers vanish silently, tell people what is changing before you roll out a new restriction. It saves a lot of "my MCP is broken" messages.

Seeing what people actually use

With OpenTelemetry export configured, set OTEL_LOG_TOOL_DETAILS=1 to include MCP server and tool names in tool events and cost and token counters. Aggregate in your collector to find out which servers are really in use before you decide what to allow.

Summary

ControlPurposeWhere it can liveHow to deliver
managed-mcp.jsonFixed set, exclusive controlSystem path onlyMDM, GPO, config management. Not server-managed.
managedMcpServersProvided remote serversManaged sources onlyServer-managed, gateway, MDM, HKLM, system file
allowedMcpServersAllowlistAny scope (enforce from managed)Managed sources for enforcement
deniedMcpServersDenylistAny scopeAs above
allowManagedMcpServersOnlyLock the allowlist to managedManaged sources onlyAs above
allowClaudeInChromeWithManagedMcpChrome alongside the fileDevice managed sources onlyMDM, HKLM, system file
allowAllClaudeAiMcpsConnectors alongside the fileManaged sources onlyAs allowedMcpServers