diff --git a/litellm/proxy/mcp_registry.json b/litellm/proxy/mcp_registry.json index 3a25ced4cff..70e81f127c9 100644 --- a/litellm/proxy/mcp_registry.json +++ b/litellm/proxy/mcp_registry.json @@ -220,7 +220,7 @@ { "name": "microsoft_365", "title": "Microsoft 365 (Graph)", - "description": "Outlook mail and calendar, OneDrive and SharePoint files, and Teams through Microsoft Graph, with each user's own Entra ID sign-in", + "description": "Outlook mail and calendar, OneDrive and SharePoint files, and Teams through Microsoft Graph, with each user's own Entra ID sign-in. Self-hosted: run ms-365-mcp-server next to the proxy and point the URL at it", "icon_url": "/ui/assets/logos/microsoft_365.svg", "category": "Productivity", "registry_url": "https://registry.modelcontextprotocol.io/servers/io.github.Softeria%2Fms-365-mcp-server", diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_discovery.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_discovery.py index 3f8d6a2be3f..a3d120caa88 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_discovery.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_discovery.py @@ -1,5 +1,6 @@ import json import os +from typing import Final import pytest @@ -99,6 +100,27 @@ class TestMCPRegistryFile: missing = expected - names assert not missing, f"Missing well-known servers: {missing}" + def test_microsoft_365_is_a_self_hosted_streamable_http_server(self, registry_path): + """The Graph server runs next to the proxy in org mode, so the entry must be streamable HTTP at /mcp.""" + with open(registry_path, "r") as f: + data = json.load(f) + entry: Final = next(s for s in data["servers"] if s["name"] == "microsoft_365") + assert entry["transport"] == "http" + assert entry["url"].endswith("/mcp") + assert entry["category"] == "Productivity" + assert "ms-365-mcp-server" in entry["registry_url"] + + def test_bundled_icons_exist(self, registry_path): + """An icon served from the proxy's own assets must ship in the built dashboard or the card renders blank.""" + with open(registry_path, "r") as f: + data = json.load(f) + logos_dir: Final = os.path.join(os.path.dirname(registry_path), "_experimental", "out", "assets", "logos") + bundled: Final = [s for s in data["servers"] if s.get("icon_url", "").startswith("/ui/assets/logos/")] + assert bundled, "at least one registry entry ships its own icon" + for server in bundled: + icon: Final = os.path.join(logos_dir, os.path.basename(server["icon_url"])) + assert os.path.isfile(icon), f"{server['name']} points at a missing icon {server['icon_url']}" + def test_env_vars_structure(self, registry_path): with open(registry_path, "r") as f: data = json.load(f)