Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions cookbook/litellm_proxy_server/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Publish MCP servers in the AI Hub

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the standalone README in #43394; the visibility dialog still provides YAML examples, canonical server ID guidance, and clearing controls


Set `litellm_settings.public_mcp_servers` to the concrete IDs of the servers you want listed in the public AI Hub. Pin `server_id` in each configuration entry so the publication list stays stable across deployments

```yaml
mcp_servers:
documentation:
server_id: documentation-mcp
url: https://mcp.example.com/mcp
transport: http
available_on_public_internet: true

litellm_settings:
public_mcp_hub_strict_whitelist: true
public_mcp_servers:
- documentation-mcp
```

Use `documentation-mcp`, the `server_id`, in the publication list. The configuration key `documentation`, display names, and aliases are not publication IDs. Database-created servers use the ID returned by `/v1/mcp/server`

The dashboard's **AI Hub > MCP Hub > Manage MCP Hub Visibility** dialog edits this same list. Its YAML example includes the selected server IDs. With database-backed configuration (`store_model_in_db: true`), a value declared in YAML is owned by that file: edit the file and reload, or remove that key from YAML to let the dashboard manage it in the database. File-backed deployments can save the list directly to their configuration file

To remove all explicit entries, save an empty selection in the dialog or configure:

```yaml
litellm_settings:
public_mcp_hub_strict_whitelist: true
public_mcp_servers: []
```

## Hub listing and network access

The **Hub listing** column in AI Hub identifies servers that appear in `/public/mcp_hub`. The dashboard derives this status from the current registry and publication settings. Setting `mcp_info.is_public` on a server does not publish it; that response field is derived metadata. `mcp_info.is_public_explicit` identifies registered servers included in the explicit publication list

Gateway cards and server details show **All Networks** when `available_on_public_internet` is enabled or the server is explicitly published in `public_mcp_servers`. They show **Internal Only** when both are false. The per-server flag defaults to `true`; explicit publication overrides a disabled flag for compatibility. Older proxies that omit the metadata needed to determine access show **Unknown**. These labels describe allowed client IPs; authentication and tool permissions still apply

The default `public_mcp_hub_strict_whitelist: true` lists only registered servers in `public_mcp_servers`. Legacy mode (`false`) additionally lists registered servers with `available_on_public_internet: true`. In legacy mode, clearing the explicit publication list leaves these automatically listed servers visible. Enable strict mode when the publication list should fully determine hub visibility
24 changes: 12 additions & 12 deletions litellm/proxy/_experimental/mcp_server/mcp_server_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6764,6 +6764,16 @@ def get_mcp_server_by_id(self, server_id: str, client_ip: str | None = None) ->
return server
return None

@staticmethod
def _is_public_mcp_server(server: MCPServer, public_ids: Container[str]) -> bool:
return server.server_id in public_ids or (
not litellm.public_mcp_hub_strict_whitelist and server.available_on_public_internet
)

def is_mcp_server_public(self, server_id: str) -> bool:
server: Final = self.registry.get(server_id) or self.config_mcp_servers.get(server_id)
return server is not None and self._is_public_mcp_server(server, litellm.public_mcp_servers or ())

Comment on lines +6767 to +6776

@ryan-crabbe-berri ryan-crabbe-berri Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these feel like they be one method, a little confusing naming here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidated the helpers into is_mcp_server_public in #43394, preserving batch lookups and strict/legacy behavior; the existing publication matrix passes

def get_public_mcp_servers(self) -> list[MCPServer]:
"""
Return the MCP servers published to the AI Hub via /v1/mcp/make_public.
Expand All @@ -6781,18 +6791,8 @@ def get_public_mcp_servers(self) -> list[MCPServer]:
deployments that relied on the OR-with-default semantics; will be
removed in a future release.
"""
if litellm.public_mcp_hub_strict_whitelist:
if litellm.public_mcp_servers is None:
return []
public_ids = set(litellm.public_mcp_servers)
return [server for server in self.get_registry().values() if server.server_id in public_ids]

public_ids = set(litellm.public_mcp_servers or [])
return [
server
for server in self.get_registry().values()
if server.available_on_public_internet or server.server_id in public_ids
]
public_ids: Final = frozenset(litellm.public_mcp_servers or ())
return [server for server in self.get_registry().values() if self._is_public_mcp_server(server, public_ids)]

def expand_permission_list(self, identifiers: list[str]) -> list[str]:
"""
Expand Down
35 changes: 16 additions & 19 deletions litellm/proxy/management_endpoints/mcp_management_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,16 @@ def _redact_mcp_credentials(
if hasattr(redacted_server, "credentials"):
setattr(redacted_server, "credentials", _preserved_admin_config_credentials(redacted_server.credentials))

return redacted_server
is_public: Final = global_mcp_server_manager.is_mcp_server_public(redacted_server.server_id)
return redacted_server.model_copy(
update={
"mcp_info": {
**(redacted_server.mcp_info or {}),
"is_public": is_public,
"is_public_explicit": is_public and redacted_server.server_id in (litellm.public_mcp_servers or ()),
}
}
)

def _preserved_admin_config_credentials(
credentials: "MCPCredentials | str | None",
Expand Down Expand Up @@ -832,10 +841,10 @@ def _sanitize_mcp_server_for_virtual_key(
sanitized.updated_at = None

# `mcp_info` is arbitrary metadata; keep only an explicit safe subset.
is_public = False
if isinstance(sanitized.mcp_info, dict):
is_public = bool(sanitized.mcp_info.get("is_public"))
sanitized.mcp_info = {"is_public": True} if is_public else None
sanitized.mcp_info = {
"is_public": (sanitized.mcp_info or {}).get("is_public") is True,
"is_public_explicit": (sanitized.mcp_info or {}).get("is_public_explicit") is True,
}

return sanitized

Expand Down Expand Up @@ -1260,14 +1269,6 @@ async def fetch_all_mcp_servers(
for server in redacted_mcp_servers:
server.connected_app_reachable = server.server_id in reachable_ids

# augment the mcp servers with public status
if litellm.public_mcp_servers is not None:
for server in redacted_mcp_servers:
if server.server_id in litellm.public_mcp_servers:
if server.mcp_info is None:
server.mcp_info = {}
server.mcp_info["is_public"] = True

# Annotate has_user_credential for BYOK servers (single batched query)
from litellm.proxy.proxy_server import prisma_client as _byok_prisma_client

Expand Down Expand Up @@ -3017,9 +3018,6 @@ async def make_mcp_servers_public(
},
)

if litellm.public_mcp_servers is None:
litellm.public_mcp_servers = []

for server_id in request.mcp_server_ids:
server = global_mcp_server_manager.get_mcp_server_by_id(server_id=server_id)
if server is None:
Expand All @@ -3028,16 +3026,15 @@ async def make_mcp_servers_public(
detail=f"MCP Server with ID {server_id} not found",
)

litellm.public_mcp_servers = request.mcp_server_ids

# Update config with new settings
if "litellm_settings" not in config or config["litellm_settings"] is None:
config["litellm_settings"] = {}

config["litellm_settings"]["public_mcp_servers"] = litellm.public_mcp_servers
config["litellm_settings"]["public_mcp_servers"] = request.mcp_server_ids

# Save the updated config
await proxy_config.save_config(new_config=config)
litellm.public_mcp_servers = request.mcp_server_ids

verbose_proxy_logger.debug(
"Updated public mcp servers to: %s by user: %s", litellm.public_mcp_servers, user_api_key_dict.user_id
Expand Down
14 changes: 13 additions & 1 deletion litellm/proxy/public_endpoints/public_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,19 @@ async def get_mcp_servers():
)

public_mcp_servers: Final = global_mcp_server_manager.get_public_mcp_servers()
return [MCPPublicServer.model_validate(server.model_dump()) for server in public_mcp_servers]
return [
MCPPublicServer.model_validate(
{
**server.model_dump(),
"mcp_info": {
**(server.mcp_info or {}),
"is_public": True,
"is_public_explicit": server.server_id in (litellm.public_mcp_servers or ()),
},
}
)
for server in public_mcp_servers
]


@router.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9385,6 +9385,60 @@ def _make_manager(self, servers):
manager.config_mcp_servers[s.server_id] = s
return manager

@pytest.mark.parametrize("registered_in", ("config", "database", "both", "neither"))
@pytest.mark.parametrize("public_ids", (None, [], ["server-id"], ["server-alias"], ["Server Name"]))
@pytest.mark.parametrize(
"strict,network_access,implicitly_public",
((True, True, False), (True, False, False), (False, True, True), (False, False, False)),
)
def test_public_status_agrees_with_hub_membership(
self,
registered_in: Literal["config", "database", "both", "neither"],
public_ids: list[str] | None,
strict: bool,
network_access: bool,
implicitly_public: bool,
) -> None:
manager: Final = MCPServerManager()
server: Final = MCPServer(
server_id="server-id",
name="server-alias",
alias="server-alias",
server_name="Server Name",
transport=MCPTransport.http,
available_on_public_internet=network_access,
mcp_info={"is_public": True, "description": "Preserve custom metadata"},
)
config_server: Final = (
server.model_copy(update={"available_on_public_internet": not network_access})
if registered_in == "both"
else server
)
manager.config_mcp_servers = (
{server.server_id: config_server} if registered_in in ("config", "both") else {}
)
manager.registry = {server.server_id: server} if registered_in in ("database", "both") else {}
original_server: Final = server.model_dump()
original_config_server: Final = config_server.model_dump()
expected_public: Final = registered_in != "neither" and (
public_ids == [server.server_id] or implicitly_public
)

with (
patch("litellm.public_mcp_servers", public_ids),
patch("litellm.public_mcp_hub_strict_whitelist", strict),
):
public_servers: Final = manager.get_public_mcp_servers()
assert manager.is_mcp_server_public(server.server_id) is expected_public
assert [item.server_id for item in public_servers] == (
[server.server_id] if expected_public else []
)
assert manager.is_mcp_server_public("server-alias") is False
assert manager.is_mcp_server_public("missing-server") is False

assert server.model_dump() == original_server
assert config_server.model_dump() == original_config_server

@patch("litellm.public_mcp_servers", None)
def test_returns_empty_when_whitelist_is_none(self):
"""No /make_public call yet → hub returns nothing, regardless of
Expand Down
Loading
Loading