ms365-mcp
Provides a minimal Microsoft 365 MCP server with fourteen hand-written tools for email, people, and drafts, designed to fit small local models.
README
ms365-mcp
A Microsoft 365 MCP server sized for small local models.
Most MCP servers are built for frontier models with large context windows, so they expose everything the vendor API offers and let the model sort it out. The reference Microsoft 365 server exposes 326 tools generated from Microsoft's Graph OpenAPI spec.
That does not work for an 8B model at 8k context, where the entire tool list sits in the system prompt and the model starts choosing at random somewhere past twenty-five tools. This server takes the opposite approach:
- Fourteen hand-written tools, with a hard ceiling of twenty. The tool list is a budget, not a feature list.
- Descriptions written for a small model, not salvaged from API metadata.
- Draft-first. Exactly one tool can transmit to another person, it takes a draft id rather than a recipient, and it is annotated destructive.
- Every tool declares its own effect through MCP
readOnlyHintanddestructiveHintannotations, so a host can classify it without guessing from the tool's name.
Built for the Edge Harness, and useful to any MCP client.
Status
Feature complete, not yet run against a real account. All fourteen tools are implemented and 171 tests pass, none of which touch the network. What has not happened is the part only a real tenant can prove: a live sign-in and a Graph call that gets answered. See docs/PLAN.md for the ten commits and the reasoning behind each decision.
| Scaffold, licence, CI | done |
| MCP protocol over stdio | done |
| The fourteen-tool surface | done, all fourteen implemented |
| Sign-in | done, untested against a live tenant |
| Microsoft Graph | done, tested against recorded shapes only |
| Retries and circuit breaker | done |
Seeing the tool surface
--list-tools starts nothing and needs no account, so it answers "what would
a host be offered?" before any of it is wired up:
.venv/bin/python -m ms365_mcp --list-tools
Add --json for a machine-readable version.
read whoami Return the display name, email address and timezone…
read resolve_person Look up a person by name or partial name and return…
read list_messages List messages in a mail folder, newest first, as se…
write create_draft Create a draft message. This does NOT send anything…
! write send_draft Send a draft that already exists, by its id. This r…
...
14 tools, 6 that change something, 4 marked destructive (!)
Handlers are stubs today: a call returns a sentence saying the tool is declared but not implemented. The surface is deliberately fixed before any of it is built, because the tool names, descriptions and effect classes are what a host classifies and a model is prompted against, and those are expensive to change later.
Signing in
Sign-in is the Microsoft device code flow, so there is no redirect URL to host and no secret to store. You need one thing first: the application id of an Azure app registration that has the delegated Graph permissions this server's tools declare.
export MS365_MCP_CLIENT_ID=<your app registration's application id>
export MS365_MCP_TENANT_ID=organizations # 'consumers' for outlook.com
.venv/bin/python -m ms365_mcp --login
Then check it, before wiring anything into a host:
.venv/bin/python -m ms365_mcp --verify-login
--verify-login prints the client id, tenant, authority, scopes and cache
location, and if an account is cached it calls Graph once. That last part is
the point. A cached token can look completely healthy and still be refused,
because a work or school tenant can withhold consent for a scope the surface
needs, and that only ever shows up as a 403 on a real request. --logout
forgets the account and removes the cache.
The token cache lives in the operating system's config directory, never in this repository. It is created with mode 0600 rather than written and then made private, since the gap between those two is a window where a refresh token is readable by every account on the machine. It is replaced by an atomic rename, so an interrupted write costs nothing.
Using it from the Edge Harness
The harness launches this server as a subprocess over stdio, so it runs under
its own interpreter and shares no dependencies with the host. That is not
incidental: the harness has a local package called mcp/ and the official SDK
on PyPI is also called mcp, so installing this server into the harness's
virtualenv shadows the harness's own package.
Its registry entry there is ms365-own, and pressing Check in the Agent
Lab run options lists every tool with its effect class. All fourteen classify
as declared, meaning the harness took them from this server's annotations
rather than guessing from tool names, and the entry needs no overrides.
In the harness's default draft mode, send_draft is withheld and the other
thirteen are available, so a run cannot reach a person.
Errors are raised as the SDK's ToolError rather than allowed to propagate.
That is worth knowing if you are writing a handler here: the SDK treats any
other exception as a crash and replaces its message with a generic one, so a
sentence like "No signed-in account, run --login" is discarded before it ever
reaches the model. ToolError is the deliberate-failure channel and its text
survives, while the result is still flagged as an error.
Security
This repository is public and holds no credential of any kind.
Authentication uses the Microsoft device-code flow. The resulting token cache
is written to the operating system's config directory with owner-only
permissions, never into this repository or its working tree. .gitignore
covers token and cache filenames anyway, and a pre-commit hook scans staged
changes for credential-shaped strings, so committing one takes deliberate
effort rather than a slip.
A line ending in # pragma: allowlist secret is skipped by the hook. That
exists so this repository's own tests can contain fake credentials, and it
leaves a marker a reviewer can see in the diff, which --no-verify does not.
Use it only for data that is provably fake.
Development
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest -q
Enable the secret-scan hook once per clone:
git config core.hooksPath .githooks
Licence
MIT. See LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.