AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Exchange Server Mcp

mcp-frankysweb-exchange-server-mcp · by FrankysWeb

Read-only MCP server for on-prem Exchange Server administration & troubleshooting (Kerberos remote PowerShell, Docker)

No reviews yet
0 installs
5 views
0.0% view→install

Install

$ agentstack add mcp-frankysweb-exchange-server-mcp

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-frankysweb-exchange-server-mcp)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Exchange Server Mcp? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Exchange Server MCP

A Model Context Protocol server that gives an LLM read-only administration and troubleshooting access to an on-premises Microsoft Exchange Server via remote PowerShell (PSRP/WinRM).

It exposes a curated set of narrow, named diagnostic tools — one per whitelisted cmdlet — so the model can answer questions like "is mail flow healthy on this server?", "when does this certificate expire?" or "why was this message delayed?" without ever being able to run arbitrary commands or change anything.

> Scope: server administration & diagnostics. It does not read users' > mail — no message bodies, no EWS. Diagnostic data (message tracking incl. > subject, calendar/rule config, statistics) is in scope, because that is what > troubleshooting needs.

Safety model

This project is built around a few non-negotiable rules:

  1. No generic "run PowerShell" tool. Every capability is one named tool that

maps to exactly one cmdlet. The model cannot execute arbitrary script.

  1. A cmdlet whitelist is authoritative. A cmdlet runs only if it is listed

in [config/cmdlet_whitelist.yaml](config/cmdlet_whitelist.yaml), and only with the parameters declared there. Everything else is rejected before it reaches the server.

  1. Read-only by default. Mutating cmdlets are blocked unless explicitly

flagged and ALLOW_WRITE_CMDLETS=true. The project ships read-only.

  1. Everything is audited. Every call (ok / blocked / error) is appended to a

JSON-lines audit log through a single chokepoint.

  1. Least privilege. Designed for a View-Only Organization Management

service account.

How it works

MCP tool  (generated from the whitelist)
   └─ Runner.execute(cmdlet, params)
        ├─ Whitelist.validate()           # reject unknown cmdlet / param / write
        ├─ ExchangeSession.run_cmdlet()   # pypsrp PSRP → Microsoft.Exchange
        └─ AuditLog.record()              # JSON-lines audit (ok / blocked / error)

Runner.execute is the single point through which all cmdlet execution flows. Tools are generated from the whitelist at startup, so the whitelist is the one source of truth for what the server can do.

The current whitelist exposes 99 read-only tools across: servers & health, certificates, databases & DAG, transport & mail flow, message tracking, client-access & virtual directories, mobile devices, recipients & groups, permissions, mailbox/calendar/rule configuration, retention, RBAC, migration and organization configuration.

Requirements

  • Docker (the supported runtime — see Why Docker below).
  • An on-prem Exchange Server reachable over WinRM/HTTP(S) (the /PowerShell

IIS endpoint).

  • A service account with View-Only Organization Management (or narrower).
  • The Exchange endpoint typically accepts Kerberos only, so the host running

the container must be able to reach the realm's KDC.

Why Docker

The Exchange /PowerShell endpoint advertises WWW-Authenticate: Kerberos and nothing else. Authenticating with explicit credentials from a non domain-joined machine is reliable from Linux via pyspnego's GSSAPI/Kerberos backend, but fights the Windows SSPI stack (which insists on resolving an SPN and fails for non-joined hosts). The Docker image bundles the Kerberos backend and generates /etc/krb5.conf from your environment at startup, so it runs anywhere Docker does.

Configuration

The server is configured entirely through environment variables — nothing is baked into the image. A .env file is just the most convenient way to provide them; anything that sets the same variables works (docker run -e VAR=value …, or a client that injects them from its own settings UI / OS keychain — see [Configuring from a client](#configuring-from-an-mcp-client)).

> In the Docker deployment there is no .env inside the container — > --env-file .env reads the file on the host and passes the values in as > environment variables. The .env file is never committed or imaged > (.gitignore + .dockerignore).

For local use, copy the template and fill in the three required values:

cp .env.example .env
# minimal .env — everything else has sensible defaults
EXCHANGE_SERVER=mail.example.com                # FQDN, not IP
EXCHANGE_USERNAME=svc-exchange-mcp@EXAMPLE.COM   # UPN form: user@REALM
EXCHANGE_PASSWORD=your-password

| Variable | Purpose | Default | |---|---|---| | EXCHANGE_SERVER | Exchange FQDN (the /PowerShell host). Use the FQDN, not the IP. | — | | EXCHANGE_USERNAME | Service account, UPN form user@REALM. | — | | EXCHANGE_PASSWORD | Service account password. | — | | EXCHANGE_AUTH | Auth mechanism (kerberos). | kerberos | | EXCHANGE_SPN_SERVICE | Kerberos SPN service class (IIS vdir → HTTP). | HTTP | | EXCHANGE_PORT / EXCHANGE_SCHEME | 80/http or 443/https. | 80 / http | | EXCHANGE_REALM / EXCHANGE_KDC | Kerberos realm / KDC. Derived from the username/server if unset. | derived | | EXCHANGE_VERIFY_SSL | Verify the TLS cert (only for https). | false | | ALLOW_WRITE_CMDLETS | Hard guard for write cmdlets. | false | | MAX_RESULT_OBJECTS | Cap objects returned per call. | 200 |

See [.env.example](.env.example) for the full annotated list.

Configuring from an MCP client

Because configuration is just environment variables, a client with a per-server settings UI — such as Claude Desktop's connector panel, which stores secrets in the OS keychain — can supply them instead of a file. Forward each variable into the container with -e VAR (no value means "inherit from whatever launched docker"):

docker run -i --rm --add-host mail.example.com:10.0.0.10 \
  -e EXCHANGE_SERVER -e EXCHANGE_USERNAME -e EXCHANGE_PASSWORD \
  -v /abs/path/logs:/app/logs exchange-server-mcp

This keeps secrets out of any file on disk. There is also a packaged Claude Desktop extension (.mcpb) with a user_config form — see [Claude Desktop extension](#claude-desktop-extension-mcpb) below.

Build & run

docker build -t exchange-server-mcp .

Smoke-test the connection (the host must resolve the Exchange FQDN — either via DNS or an --add-host mapping to its IP):

docker run --rm --add-host mail.example.com:10.0.0.10 \
  --env-file .env exchange-server-mcp python scripts/conn_test.py

Connecting an MCP client

The server speaks MCP over stdio; the client launches the container per session. Note -i, and bind-mount a host folder for the audit log so it lands as a normal file on disk (audit.log appears directly in that folder). Replace /abs/path/logs with a real path — on Windows use e.g. "C:/Users/you/exchange-logs:/app/logs":

docker run -i --rm \
  --add-host mail.example.com:10.0.0.10 \
  --env-file /abs/path/.env \
  -v /abs/path/logs:/app/logs \
  exchange-server-mcp

Claude Code:

claude mcp add exchange -- docker run -i --rm \
  --add-host mail.example.com:10.0.0.10 \
  --env-file /abs/path/.env -v /abs/path/logs:/app/logs exchange-server-mcp

Codex (OpenAI)codex mcp add exchange -- docker run …, or add to ~/.codex/config.toml:

[mcp_servers.exchange]
command = "docker"
args = [
  "run", "-i", "--rm",
  "--add-host", "mail.example.com:10.0.0.10",
  "--env-file", "/abs/path/.env",
  "-v", "/abs/path/logs:/app/logs",
  "exchange-server-mcp",
]

Claude Desktop (claude_desktop_config.json) — or use the one-click [extension](#claude-desktop-extension-mcpb) below:

{
  "mcpServers": {
    "exchange": {
      "command": "docker",
      "args": ["run", "-i", "--rm",
        "--add-host", "mail.example.com:10.0.0.10",
        "--env-file", "/abs/path/.env",
        "-v", "/abs/path/logs:/app/logs",
        "exchange-server-mcp"]
    }
  }
}

Any other client that launches a stdio command works the same way (Cursor, Windsurf, VS Code, …): point it at the docker run … exchange-server-mcp command.

ChatGPT is the exception: it only supports remote (HTTPS) MCP connectors in Developer Mode, not local stdio. This server is stdio/Docker, so it cannot be added directly. To use it with ChatGPT you would have to expose it over HTTPS — e.g. with an mcp-remote bridge or a tunnel (ngrok / Cloudflare Tunnel) — and register that URL as a custom connector. That is an advanced, self-hosted setup and not a first-class path here; a native HTTP transport is possible future work.

Claude Desktop extension (.mcpb)

For Claude Desktop you can install this as a one-click extension instead of editing config: Claude shows a settings form (server, account, password) and stores the password in the OS keychain. The manifest lives in [extension/manifest.json](extension/manifest.json).

The extension launches docker run … exchange-server-mcp, so first build the image locally (until a public image is published):

docker build -t exchange-server-mcp .

Then install the bundled [exchange-server-mcp.mcpb](exchange-server-mcp.mcpb) (in the repo root): open it with Claude Desktop, or Settings → Extensions → Install. Fill in the form and save. The fields map to the same environment variables described above — the password is stored in the OS keychain and passed through with -e, so it never appears on disk or in the process arguments.

> Maintainers: if you change extension/manifest.json, re-pack and > re-commit the bundle (needs Node.js): > ``bash > npx @anthropic-ai/mcpb validate extension/manifest.json > npx @anthropic-ai/mcpb pack extension exchange-server-mcp.mcpb > ``

> The form asks for a DNS server (usually a domain controller) and the > Exchange server IP. The DNS server lets the container resolve the Exchange > server, the KDC and Kerberos records — important when the KDC is a separate > domain controller (it won't resolve otherwise). The IP additionally pins the > Exchange name via --add-host. For the CLI paths above, the equivalents are > --dns and --add-host :.

Adding a cmdlet

Tools are generated from the whitelist, so adding a capability is a single edit to [config/cmdlet_whitelist.yaml](config/cmdlet_whitelist.yaml):

Get-MailboxDatabase:
  write: false
  description: "Mailbox database configuration. Status adds mount/size fields."
  allowed_params: [Identity, Server, Status]
  param_types: { Identity: str, Server: str, Status: switch }

On the next start a tool get_mailbox_database(identity=None, server=None, status=False) is registered automatically — snake_case args are mapped back to the cmdlet's PascalCase parameters, and switch params become booleans.

Keep new cmdlets within the read-only, no-message-body scope.

Development

python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"

python -m pytest -q     # unit tests (mocked — no live server needed)
ruff check . && ruff format .

The test suite covers whitelist validation, the Runner (auditing + output cap), serialization (switch encoding, CLIXML collection extraction, certificate parsing) and tool generation — all without a server. scripts/smoke_whitelist.py is the manual end-to-end check against a real box.

Project layout

src/exchange_mcp/
  server.py        entry point — wires everything together
  config.py        settings from .env
  connection.py    pypsrp PSRP session + result serialization
  whitelist.py     cmdlet + parameter validation (safety core)
  runner.py        validate → audit → execute chokepoint + output cap
  audit.py         JSON-lines audit log
  tools/diagnostics.py   generates one MCP tool per whitelist entry
config/cmdlet_whitelist.yaml   the authoritative cmdlet list
docker/entrypoint.sh           generates /etc/krb5.conf from env
tests/                         pytest suite

Security notes

  • .env holds credentials and is never committed or baked into the image.
  • The container runs as a non-root user (uid 10001). Bind-mount a host

folder for the audit log (-v /abs/path/logs:/app/logs); Docker Desktop makes bind mounts writable by the container user, so audit.log is written straight to that folder on the host.

  • The audit log records cmdlet, parameters and outcome for every call — treat it

as sensitive (it can contain recipient addresses and message subjects) and mount it to durable storage.

  • Run the container with a least-privilege service account; the whitelist is the

backstop, not the only control.

  • The server reconnects once automatically if the WinRM session drops (idle

timeout, app-pool recycle); all whitelisted cmdlets are read-only, so the retry is safe.

Troubleshooting

Most setup problems are Kerberos. Run the bundled diagnostic inside the container — it checks config, DNS, KDC reachability, a TGT (kinit), the SPN ticket (kvno) and the actual pyspnego/gssapi path the server uses, with a KRB5 trace per step:

docker run --rm --dns  --add-host : \
  -e EXCHANGE_SERVER= -e EXCHANGE_USERNAME= \
  -e EXCHANGE_PASSWORD='...' -e EXCHANGE_REALM= -e EXCHANGE_KDC= \
  exchange-server-mcp python scripts/troubleshoot_kerberos.py

Common causes of Matching credential not found / SpnegoError:

  • Username case. Use the account's exact case as stored in AD. kinit

tolerates a mismatch but pyspnego/gssapi does not — the script warns if the Kerberos salt shows a different case than you entered.

  • Realm ≠ NetBIOS. The part after @ must be the DNS realm

(user@CORP.EXAMPLE.COM), not the NetBIOS name (user@CORP).

  • KDC not resolvable. The KDC is usually a separate domain controller; set

a DNS server (--dns) so the container can resolve it.

License

[MIT](LICENSE) © FrankysWeb


Built and maintained by FrankysWeb — a German blog on Microsoft Exchange, Windows Server and related infrastructure.

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.