Skip to main content
Glama
README.md
# 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 `readOnlyHint` and
  `destructiveHint` annotations, so a host can classify it without guessing
  from the tool's name.

Built for the [Edge Harness](https://github.com/EdgeHarness/Brick-Agent-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](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:

```bash
.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.

```bash
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:

```bash
.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

```bash
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest -q
```

Enable the secret-scan hook once per clone:

```bash
git config core.hooksPath .githooks
```

## Licence

MIT. See [LICENSE](LICENSE).

TDQS

A4.4/5.0

Scored across 14 tools

Disambiguation5/5

Each tool targets a distinct resource and action: identity, people lookup, mail read/draft/send, and calendar read/find/write. The only close pair, create_draft and reply_draft, is clearly separated by new-vs-reply intent.

Naming Consistency4/5

Almost all tools follow a consistent verb_noun pattern (list_messages, create_draft, cancel_event). The lone exception is whoami, which is a conventional single-word command but breaks the otherwise uniform pattern.

Tool Count5/5

14 tools is a well-scoped size for a mail-plus-calendar server. Each tool covers a necessary step in reading, creating, and sending mail or managing calendar events, with no redundant utilities.

Completeness4/5

Calendar coverage is complete with list/get/find/create/update/cancel, and mail covers read, draft, reply-draft, and send. Minor gaps such as updating/deleting drafts or searching messages exist, but agents can work around them with the provided tools.

Maintenance

ActivityMaintained
ResponsivenessNo issues