From b9e71e990aedac89a8cf5da2a47aca51abe37076 Mon Sep 17 00:00:00 2001
From: "devin-ai-integration[bot]"
<158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Fri, 2 Oct 2026 02:50:29 +0000
Subject: [PATCH] feat(mcp): add Microsoft 365 (Graph) server to the MCP
catalog (#43099)
* feat(mcp): add Microsoft 365 (Graph) server to the MCP catalog
* fix(mcp): signpost the self-hosted Microsoft 365 URL and pin the catalog entry in tests
* test(mcp): pin both shipped copies of a catalog icon to the same bytes
---------
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
---
.../out/assets/logos/microsoft_365.svg | 1 +
litellm/proxy/mcp_registry.json | 11 ++++++
.../mcp_server/test_mcp_discovery.py | 35 ++++++++++++++++++-
.../public/assets/logos/microsoft_365.svg | 1 +
4 files changed, 47 insertions(+), 1 deletion(-)
create mode 100644 litellm/proxy/_experimental/out/assets/logos/microsoft_365.svg
create mode 100644 ui/litellm-dashboard/public/assets/logos/microsoft_365.svg
diff --git a/litellm/proxy/_experimental/out/assets/logos/microsoft_365.svg b/litellm/proxy/_experimental/out/assets/logos/microsoft_365.svg
new file mode 100644
index 00000000000..e053ac831fb
--- /dev/null
+++ b/litellm/proxy/_experimental/out/assets/logos/microsoft_365.svg
@@ -0,0 +1 @@
+
diff --git a/litellm/proxy/mcp_registry.json b/litellm/proxy/mcp_registry.json
index b117f35600d..70e81f127c9 100644
--- a/litellm/proxy/mcp_registry.json
+++ b/litellm/proxy/mcp_registry.json
@@ -217,6 +217,17 @@
{"name": "GOOGLE_CLIENT_SECRET", "description": "Google OAuth Client Secret", "secret": true}
]
},
+ {
+ "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. 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",
+ "transport": "http",
+ "url": "http://localhost:3000/mcp",
+ "env_vars": []
+ },
{
"name": "obsidian",
"title": "Obsidian",
diff --git a/tests/unit/proxy/_experimental/mcp_server/test_mcp_discovery.py b/tests/unit/proxy/_experimental/mcp_server/test_mcp_discovery.py
index 43cf35c152d..d35cb7234dc 100644
--- a/tests/unit/proxy/_experimental/mcp_server/test_mcp_discovery.py
+++ b/tests/unit/proxy/_experimental/mcp_server/test_mcp_discovery.py
@@ -1,5 +1,6 @@
import json
import os
+from typing import Final
import pytest
@@ -95,10 +96,42 @@ class TestMCPRegistryFile:
with open(registry_path, "r") as f:
data = json.load(f)
names = {s["name"] for s in data["servers"]}
- expected = {"github", "slack", "postgresql", "snowflake", "atlassian"}
+ expected = {"github", "slack", "postgresql", "snowflake", "atlassian", "microsoft_365"}
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 ships twice, as the built copy the wheel packages and as
+ the dashboard source copy every Docker image rebuilds from. Both must exist and match or a card goes blank."""
+ with open(registry_path, "r") as f:
+ data = json.load(f)
+ proxy_dir: Final = os.path.dirname(registry_path)
+ built_logos_dir: Final = os.path.join(proxy_dir, "_experimental", "out", "assets", "logos")
+ source_logos_dir: Final = os.path.join(
+ proxy_dir, "..", "..", "ui", "litellm-dashboard", "public", "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:
+ file_name: Final = os.path.basename(server["icon_url"])
+ built: Final = os.path.join(built_logos_dir, file_name)
+ source: Final = os.path.join(source_logos_dir, file_name)
+ assert os.path.isfile(built), f"{server['name']}: {server['icon_url']} missing from the built dashboard"
+ assert os.path.isfile(source), f"{server['name']}: {server['icon_url']} missing from the dashboard source"
+ with open(built, "rb") as built_file, open(source, "rb") as source_file:
+ same_bytes: Final = built_file.read() == source_file.read()
+ assert same_bytes, f"{server['name']}: built and source copies of {file_name} differ"
+
def test_env_vars_structure(self, registry_path):
with open(registry_path, "r") as f:
data = json.load(f)
diff --git a/ui/litellm-dashboard/public/assets/logos/microsoft_365.svg b/ui/litellm-dashboard/public/assets/logos/microsoft_365.svg
new file mode 100644
index 00000000000..e053ac831fb
--- /dev/null
+++ b/ui/litellm-dashboard/public/assets/logos/microsoft_365.svg
@@ -0,0 +1 @@
+