diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md
index 8c11b8fd654..b25c58d3f93 100644
--- a/docs/my-website/docs/mcp.md
+++ b/docs/my-website/docs/mcp.md
@@ -198,6 +198,7 @@ mcp_servers:
- `http` - Streamable HTTP transport
- `stdio` - Standard Input/Output transport
- **Command**: The command to execute for stdio transport (required for stdio)
+- **allow_all_keys**: Set to `true` to make the server available to every LiteLLM API key, even if the key/team doesn't list the server in its MCP permissions.
- **Args**: Array of arguments to pass to the command (optional for stdio)
- **Env**: Environment variables to set for the stdio process (optional for stdio)
- **Description**: Optional description for the server
diff --git a/docs/my-website/docs/mcp_control.md b/docs/my-website/docs/mcp_control.md
index c8c3d8e10f3..2a3a7bc5a05 100644
--- a/docs/my-website/docs/mcp_control.md
+++ b/docs/my-website/docs/mcp_control.md
@@ -13,6 +13,7 @@ LiteLLM provides fine-grained permission management for MCP servers, allowing yo
- **Restrict MCP access by entity**: Control which keys, teams, or organizations can access specific MCP servers
- **Tool-level filtering**: Automatically filter available tools based on entity permissions
- **Centralized control**: Manage all MCP permissions from the LiteLLM Admin UI or API
+- **One-click public MCPs**: Mark specific servers as available to every LiteLLM API key when you don't need per-key restrictions
This ensures that only authorized entities can discover and use MCP tools, providing an additional security layer for your MCP infrastructure.
@@ -95,6 +96,48 @@ mcp_servers:
- If you specify both `allowed_tools` and `disallowed_tools`, the allowed list takes priority
- Tool names are case-sensitive
+## Public MCP Servers (allow_all_keys)
+
+Some MCP servers are meant to be shared broadly—think internal knowledge bases, calendar integrations, or other low-risk utilities where every team should be able to connect without requesting access. Instead of adding those servers to every key, team, or organization, enable the new `allow_all_keys` toggle.
+
+
+
+
+1. Open **MCP Servers → Add / Edit** in the Admin UI.
+2. Expand **Permission Management / Access Control**.
+3. Toggle **Allow All LiteLLM Keys** on.
+
+
+
+The toggle makes the server “public” without touching existing access groups.
+
+
+
+
+Set `allow_all_keys: true` to mark the server as public:
+
+```yaml title="Make an MCP server public" showLineNumbers
+mcp_servers:
+ deepwiki:
+ url: https://mcp.deepwiki.com/mcp
+ allow_all_keys: true
+```
+
+
+
+
+### When to use it
+
+- You have shared MCP utilities where fine-grained ACLs would only add busywork.
+- You want a “default enabled” experience for internal users, while still being able to layer tool-level restrictions.
+- You’re onboarding new teams and want the safest MCPs available out of the box.
+
+Once enabled, LiteLLM automatically includes the server for every key during tool discovery/calls—no extra virtual-key or team configuration is required.
+
---
## Allow/Disallow MCP Tool Parameters
diff --git a/docs/my-website/img/mcp_oauth.png b/docs/my-website/img/mcp_oauth.png
new file mode 100644
index 00000000000..e504ccc86bb
Binary files /dev/null and b/docs/my-website/img/mcp_oauth.png differ
diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260105151539_add_allow_all_keys_to_mcp_servers/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260105151539_add_allow_all_keys_to_mcp_servers/migration.sql
new file mode 100644
index 00000000000..8d3e02bd051
--- /dev/null
+++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260105151539_add_allow_all_keys_to_mcp_servers/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "LiteLLM_MCPServerTable" ADD COLUMN "allow_all_keys" BOOLEAN NOT NULL DEFAULT false;
+
diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma
index ea47b6ed03b..e565135bbc4 100644
--- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma
+++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma
@@ -211,6 +211,7 @@ model LiteLLM_MCPServerTable {
authorization_url String?
token_url String?
registration_url String?
+ allow_all_keys Boolean @default(false)
}
// Generate Tokens for Proxy
@@ -748,4 +749,4 @@ model LiteLLM_SkillsTable {
created_by String?
updated_at DateTime @default(now()) @updatedAt
updated_by String?
-}
\ No newline at end of file
+}
diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
index d193fc27fb9..a5ac966062e 100644
--- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
+++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
@@ -260,6 +260,7 @@ class MCPServerManager:
allowed_params=server_config.get("allowed_params", None),
access_groups=server_config.get("access_groups", None),
static_headers=server_config.get("static_headers", None),
+ allow_all_keys=bool(server_config.get("allow_all_keys", False)),
)
self.config_mcp_servers[server_id] = new_server
@@ -549,6 +550,7 @@ class MCPServerManager:
access_groups=getattr(mcp_server, "mcp_access_groups", None),
allowed_tools=getattr(mcp_server, "allowed_tools", None),
disallowed_tools=getattr(mcp_server, "disallowed_tools", None),
+ allow_all_keys=mcp_server.allow_all_keys,
)
return new_server
@@ -581,6 +583,14 @@ class MCPServerManager:
all_servers = list(self.get_registry().values())
return {server.server_id for server in all_servers}
+ def get_allow_all_keys_server_ids(self) -> List[str]:
+ """Return server IDs that bypass per-key restrictions."""
+ return [
+ server.server_id
+ for server in self.get_registry().values()
+ if server.allow_all_keys
+ ]
+
async def get_allowed_mcp_servers(
self, user_api_key_auth: Optional[UserAPIKeyAuth] = None
) -> List[str]:
@@ -593,6 +603,8 @@ class MCPServerManager:
if user_api_key_auth and _user_has_admin_view(user_api_key_auth):
return list(self.get_registry().keys())
+ allow_all_server_ids = self.get_allow_all_keys_server_ids()
+
try:
allowed_mcp_servers = await MCPRequestHandler.get_allowed_mcp_servers(
user_api_key_auth
@@ -600,14 +612,17 @@ class MCPServerManager:
verbose_logger.debug(
f"Allowed MCP Servers for user api key auth: {allowed_mcp_servers}"
)
- if len(allowed_mcp_servers) == 0:
+ combined_servers = set(allowed_mcp_servers)
+ combined_servers.update(allow_all_server_ids)
+
+ if len(combined_servers) == 0:
verbose_logger.debug(
"No allowed MCP Servers found for user api key auth."
)
- return allowed_mcp_servers
+ return list(combined_servers)
except Exception as e:
verbose_logger.warning(f"Failed to get allowed MCP servers: {str(e)}.")
- return []
+ return allow_all_server_ids
async def get_tools_for_server(self, server_id: str) -> List[MCPTool]:
"""
@@ -2238,6 +2253,7 @@ class MCPServerManager:
authorization_url=server.authorization_url,
token_url=server.token_url,
registration_url=server.registration_url,
+ allow_all_keys=server.allow_all_keys,
)
async def get_all_mcp_servers_with_health_and_teams(
@@ -2331,6 +2347,7 @@ class MCPServerManager:
authorization_url=server.authorization_url,
token_url=server.token_url,
registration_url=server.registration_url,
+ allow_all_keys=server.allow_all_keys,
)
list_mcp_servers.append(mcp_server_table)
diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py
index 7abfe7a96bc..fa32f60c073 100644
--- a/litellm/proxy/_types.py
+++ b/litellm/proxy/_types.py
@@ -1037,6 +1037,7 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase):
authorization_url: Optional[str] = None
token_url: Optional[str] = None
registration_url: Optional[str] = None
+ allow_all_keys: bool = False
@model_validator(mode="before")
@classmethod
@@ -1097,6 +1098,7 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase):
authorization_url: Optional[str] = None
token_url: Optional[str] = None
registration_url: Optional[str] = None
+ allow_all_keys: bool = False
@model_validator(mode="before")
@classmethod
@@ -1149,6 +1151,7 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase):
authorization_url: Optional[str] = None
token_url: Optional[str] = None
registration_url: Optional[str] = None
+ allow_all_keys: bool = False
class MakeMCPServersPublicRequest(LiteLLMPydanticObjectBase):
@@ -3821,4 +3824,4 @@ class LiteLLM_ManagedVectorStoresTable(LiteLLMPydanticObjectBase):
class ResponseLiteLLM_ManagedVectorStore(TypedDict, total=False):
- vector_store: LiteLLM_ManagedVectorStoresTable
\ No newline at end of file
+ vector_store: LiteLLM_ManagedVectorStoresTable
diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py
index 9111f53a517..a871a6637a2 100644
--- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py
+++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py
@@ -212,6 +212,7 @@ if MCP_AVAILABLE:
authorization_url=payload.authorization_url,
token_url=payload.token_url,
registration_url=payload.registration_url,
+ allow_all_keys=payload.allow_all_keys,
)
def get_prisma_client_or_throw(message: str):
diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma
index ea47b6ed03b..e565135bbc4 100644
--- a/litellm/proxy/schema.prisma
+++ b/litellm/proxy/schema.prisma
@@ -211,6 +211,7 @@ model LiteLLM_MCPServerTable {
authorization_url String?
token_url String?
registration_url String?
+ allow_all_keys Boolean @default(false)
}
// Generate Tokens for Proxy
@@ -748,4 +749,4 @@ model LiteLLM_SkillsTable {
created_by String?
updated_at DateTime @default(now()) @updatedAt
updated_by String?
-}
\ No newline at end of file
+}
diff --git a/litellm/types/mcp_server/mcp_server_manager.py b/litellm/types/mcp_server/mcp_server_manager.py
index 869037546ce..96fd79f466b 100644
--- a/litellm/types/mcp_server/mcp_server_manager.py
+++ b/litellm/types/mcp_server/mcp_server_manager.py
@@ -7,12 +7,14 @@ from litellm.proxy._types import MCPAuthType, MCPTransportType
# MCPInfo now allows arbitrary additional fields for custom metadata
MCPInfo = Dict[str, Any]
+
class MCPOAuthMetadata(BaseModel):
scopes: Optional[List[str]] = None
authorization_url: Optional[str] = None
token_url: Optional[str] = None
registration_url: Optional[str] = None
+
class MCPServer(BaseModel):
server_id: str
name: str
@@ -47,4 +49,5 @@ class MCPServer(BaseModel):
args: Optional[List[str]] = None
env: Optional[Dict[str, str]] = None
access_groups: Optional[List[str]] = None
+ allow_all_keys: bool = False
model_config = ConfigDict(arbitrary_types_allowed=True)
diff --git a/schema.prisma b/schema.prisma
index ea47b6ed03b..e565135bbc4 100644
--- a/schema.prisma
+++ b/schema.prisma
@@ -211,6 +211,7 @@ model LiteLLM_MCPServerTable {
authorization_url String?
token_url String?
registration_url String?
+ allow_all_keys Boolean @default(false)
}
// Generate Tokens for Proxy
@@ -748,4 +749,4 @@ model LiteLLM_SkillsTable {
created_by String?
updated_at DateTime @default(now()) @updatedAt
updated_by String?
-}
\ No newline at end of file
+}
diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
index a1fbddec586..8062243dfdd 100644
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py
@@ -1057,6 +1057,110 @@ async def test_list_tools_multiple_servers_prefixed_names():
assert names == ["jira-toolA", "zapier-toolA"]
+@pytest.mark.asyncio
+async def test_mcp_manager_allows_public_servers_without_permissions():
+ try:
+ from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
+ MCPServerManager,
+ )
+ from litellm.types.mcp_server.mcp_server_manager import MCPServer
+ from litellm.proxy._types import MCPTransport
+ except ImportError:
+ pytest.skip("MCP server not available")
+
+ manager = MCPServerManager()
+ public_server = MCPServer(
+ server_id="public",
+ name="public",
+ transport=MCPTransport.http,
+ allow_all_keys=True,
+ )
+ manager.registry = {public_server.server_id: public_server}
+
+ with patch(
+ "litellm.proxy.management_endpoints.common_utils._user_has_admin_view",
+ return_value=False,
+ ), patch(
+ "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPRequestHandler.get_allowed_mcp_servers",
+ AsyncMock(return_value=[]),
+ ):
+ allowed = await manager.get_allowed_mcp_servers(UserAPIKeyAuth())
+
+ assert allowed == ["public"]
+
+
+@pytest.mark.asyncio
+async def test_mcp_manager_returns_public_when_permission_lookup_fails():
+ try:
+ from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
+ MCPServerManager,
+ )
+ from litellm.types.mcp_server.mcp_server_manager import MCPServer
+ from litellm.proxy._types import MCPTransport
+ except ImportError:
+ pytest.skip("MCP server not available")
+
+ manager = MCPServerManager()
+ public_server = MCPServer(
+ server_id="public",
+ name="public",
+ transport=MCPTransport.http,
+ allow_all_keys=True,
+ )
+ manager.registry = {public_server.server_id: public_server}
+
+ with patch(
+ "litellm.proxy.management_endpoints.common_utils._user_has_admin_view",
+ return_value=False,
+ ), patch(
+ "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPRequestHandler.get_allowed_mcp_servers",
+ AsyncMock(side_effect=Exception("boom")),
+ ):
+ allowed = await manager.get_allowed_mcp_servers(UserAPIKeyAuth())
+
+ assert allowed == ["public"]
+
+
+@pytest.mark.asyncio
+async def test_mcp_manager_merges_public_and_restricted_servers():
+ try:
+ from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
+ MCPServerManager,
+ )
+ from litellm.types.mcp_server.mcp_server_manager import MCPServer
+ from litellm.proxy._types import MCPTransport
+ except ImportError:
+ pytest.skip("MCP server not available")
+
+ manager = MCPServerManager()
+ public_server = MCPServer(
+ server_id="public",
+ name="public",
+ transport=MCPTransport.http,
+ allow_all_keys=True,
+ )
+ scoped_server = MCPServer(
+ server_id="restricted",
+ name="restricted",
+ transport=MCPTransport.http,
+ )
+ manager.registry = {
+ public_server.server_id: public_server,
+ scoped_server.server_id: scoped_server,
+ }
+
+ with patch(
+ "litellm.proxy.management_endpoints.common_utils._user_has_admin_view",
+ return_value=False,
+ ), patch(
+ "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPRequestHandler.get_allowed_mcp_servers",
+ AsyncMock(return_value=["restricted"]),
+ ):
+ allowed = await manager.get_allowed_mcp_servers(UserAPIKeyAuth())
+
+ assert set(allowed) == {"public", "restricted"}
+
+
@pytest.mark.asyncio
async def test_call_mcp_tool_user_unauthorized_access():
"""Test that a user cannot call a tool from a server they don't have access to"""
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.test.tsx
new file mode 100644
index 00000000000..3784680062c
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.test.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, it, expect } from "vitest";
+import { Form } from "antd";
+
+import MCPPermissionManagement from "./MCPPermissionManagement";
+
+const defaultProps = {
+ availableAccessGroups: [],
+ mcpServer: null,
+ searchValue: "",
+ setSearchValue: () => {},
+ getAccessGroupOptions: () => [],
+};
+
+describe("MCPPermissionManagement", () => {
+const expandPanel = async () => {
+ const user = userEvent.setup();
+ const headerButton = screen.getByRole("button", {
+ name: /permission management/i,
+ });
+ await user.click(headerButton);
+ return user;
+};
+
+const renderWithForm = (props = {}) => {
+ const Wrapper: React.FC = ({ children }) => {
+ const [form] = Form.useForm();
+ return (
+
+ );
+ };
+
+ return render(
+
+
+ ,
+ );
+};
+
+ it("should default allow_all_keys switch to unchecked for new servers", async () => {
+ renderWithForm();
+ await expandPanel();
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("aria-checked", "false");
+ });
+
+ it("should reflect allow_all_keys when editing an existing server", async () => {
+ renderWithForm({
+ mcpServer: {
+ server_id: "server-1",
+ url: "https://example.com",
+ created_at: "2024-01-01T00:00:00Z",
+ created_by: "user",
+ updated_at: "2024-01-01T00:00:00Z",
+ updated_by: "user",
+ allow_all_keys: true,
+ },
+ });
+
+ const user = await expandPanel();
+ const toggle = screen.getByRole("switch");
+ expect(toggle).toHaveAttribute("aria-checked", "true");
+
+ await user.click(toggle);
+ expect(toggle).toHaveAttribute("aria-checked", "false");
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx
index 9286e4825cf..efc34e32672 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx
@@ -1,5 +1,5 @@
import React, { useEffect } from "react";
-import { Form, Select, Tooltip, Collapse, Input, Space, Button } from "antd";
+import { Form, Select, Tooltip, Collapse, Input, Space, Button, Switch } from "antd";
import { InfoCircleOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { MCPServer } from "./types";
const { Panel } = Collapse;
@@ -38,6 +38,11 @@ const MCPPermissionManagement: React.FC = ({
}));
form.setFieldValue("static_headers", staticHeaders);
}
+ if (typeof mcpServer.allow_all_keys === "boolean") {
+ form.setFieldValue("allow_all_keys", mcpServer.allow_all_keys);
+ }
+ } else {
+ form.setFieldValue("allow_all_keys", false);
}
}, [mcpServer, form]);
@@ -57,6 +62,26 @@ const MCPPermissionManagement: React.FC = ({
className="border-0"
>
+
+
+
+ Allow All LiteLLM Keys
+
+
+
+
+
Enable if this server should be "public" to all keys.
+
+
+
+
+
+
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
index e9f9636bf2c..d72b7c4f676 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx
@@ -188,6 +188,7 @@ const CreateMCPServer: React.FC = ({
static_headers: staticHeadersList,
stdio_config: rawStdioConfig,
credentials: credentialValues,
+ allow_all_keys: allowAllKeysRaw,
...restValues
} = values;
@@ -278,6 +279,7 @@ const CreateMCPServer: React.FC = ({
mcp_access_groups: accessGroups,
alias: restValues.alias,
allowed_tools: allowedTools.length > 0 ? allowedTools : null,
+ allow_all_keys: Boolean(allowAllKeysRaw),
static_headers: staticHeaders,
};
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
index b4486bdfb23..82f85f75eda 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx
@@ -286,7 +286,12 @@ const MCPServerEdit: React.FC = ({
if (!accessToken) return;
try {
// Ensure access groups is always a string array
- const { static_headers: staticHeadersList, credentials: credentialValues, ...restValues } = values;
+ const {
+ static_headers: staticHeadersList,
+ credentials: credentialValues,
+ allow_all_keys: allowAllKeysRaw,
+ ...restValues
+ } = values;
const accessGroups = (restValues.mcp_access_groups || []).map((g: any) =>
typeof g === "string" ? g : g.name || String(g),
@@ -339,6 +344,7 @@ const MCPServerEdit: React.FC = ({
allowed_tools: allowedTools.length > 0 ? allowedTools : null,
disallowed_tools: restValues.disallowed_tools || [],
static_headers: staticHeaders,
+ allow_all_keys: Boolean(allowAllKeysRaw ?? mcpServer.allow_all_keys),
};
const includeCredentials = restValues.auth_type && AUTH_TYPES_REQUIRING_CREDENTIALS.includes(restValues.auth_type);
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx
index 541c0bbb2f3..6a4e9c105ff 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx
@@ -229,6 +229,25 @@ export const MCPServerView: React.FC = ({
Auth Type
{handleAuth(mcpServer.auth_type)}
+
+
Allow All LiteLLM Keys
+
+ {mcpServer.allow_all_keys ? (
+
+ Enabled
+
+ ) : (
+
+ Disabled
+
+ )}
+ {mcpServer.allow_all_keys && (
+
+ All keys can access this MCP server
+
+ )}
+
+
Access Groups
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx
index 0d21f04779b..cf938e21b3a 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx
@@ -152,6 +152,7 @@ export interface MCPServer {
teams?: Team[];
mcp_access_groups?: string[];
allowed_tools?: string[];
+ allow_all_keys?: boolean;
}
export interface MCPServerProps {