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>
This commit is contained in:
devin-ai-integration[bot] 2026-10-02 02:50:29 +00:00 • committed by GitHub
parent d729f975aa
commit b9e71e990a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 47 additions and 1 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Microsoft 365"><rect x="1" y="1" width="10.5" height="10.5" fill="#F25022"/><rect x="12.5" y="1" width="10.5" height="10.5" fill="#7FBA00"/><rect x="1" y="12.5" width="10.5" height="10.5" fill="#00A4EF"/><rect x="12.5" y="12.5" width="10.5" height="10.5" fill="#FFB900"/></svg>

After

Width:  |  Height:  |  Size: 361 B

View file

@ -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",

View file

@ -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)

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Microsoft 365"><rect x="1" y="1" width="10.5" height="10.5" fill="#F25022"/><rect x="12.5" y="1" width="10.5" height="10.5" fill="#7FBA00"/><rect x="1" y="12.5" width="10.5" height="10.5" fill="#00A4EF"/><rect x="12.5" y="12.5" width="10.5" height="10.5" fill="#FFB900"/></svg>

After

Width:  |  Height:  |  Size: 361 B