# Minerva MCP **Microsoft 365 and Azure administration for AI assistants — including the parts Microsoft Graph cannot reach.** [![CI](https://github.com/HoussemMak/minerva-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/HoussemMak/minerva-mcp/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![MCP 2026-07-28](https://img.shields.io/badge/MCP-2026--07--28-6b46c1.svg)](https://modelcontextprotocol.io/specification/2026-07-28) An [MCP](https://modelcontextprotocol.io) server that lets Claude, Copilot, VS Code or any MCP client query and administer a Microsoft 365 tenant in plain language. --- ## Why another Microsoft 365 MCP server? There are already good ones, and this is not trying to replace them. | | [Lokka](https://github.com/merill/lokka) | [ms-365-mcp-server](https://github.com/softeria/ms-365-mcp-server) | **Minerva MCP** | |---|:---:|:---:|:---:| | Microsoft Graph | ✅ | ✅ 300+ tools | ✅ generic call | | Azure Resource Manager | ✅ | — | ✅ | | **Exchange Online cmdlets** | — | — | ✅ | | **SharePoint / PnP cmdlets** | — | — | ✅ | | **Teams Admin cmdlets** | — | — | ✅ | | **Purview / compliance cmdlets** | — | — | ✅ | | Risk classification per action | — | — | ✅ | | Human approval before writes | — | read-only switch | ✅ native to the protocol | | MCP 2026-07-28 (stateless) | — | — | ✅ | **If you only need Graph, use Lokka or ms-365-mcp-server.** They are excellent, more mature, and more widely used. Lokka in particular is written by a Microsoft Entra product manager. Reach for this one when you hit the wall everyone hits eventually: **a large part of Microsoft 365 administration has no Graph API.** Mailbox permissions, transport rules, message trace, retention policies, sensitivity labels, Teams calling policies, SharePoint term stores, tenant-wide Exchange settings — those live in PowerShell and nowhere else. This server exposes them, with a risk classification and a confirmation step in front of anything that writes. --- ## How it fits together ```mermaid flowchart LR A["AI client
Claude · VS Code · Copilot Studio"] B["minerva-mcp"] C{"Risk
classifier"} D["Human
confirmation"] G["Microsoft Graph
Azure ARM · Search"] P["PowerShell 7
Exchange · PnP · Teams · Purview"] M["Microsoft
authorizes the token"] A -->|"stdio or HTTP"| B B --> C C -->|"read"| G C -->|"write"| D D -->|"signed mandate"| G D -->|"signed mandate"| P C -->|"read"| P G --> M P --> M ``` Reads go straight through. Writes stop at a classifier, come back to you as an `input_required` describing the action and its consequences, and only run once you confirm. Microsoft still has the final word: the server forwards a token, it never grants access. ## Install Nothing to clone. Add it to your client and sign in on first use. ### Claude Desktop `claude_desktop_config.json`: ```json { "mcpServers": { "minerva": { "command": "npx", "args": ["-y", "minerva-mcp", "--stdio"] } } } ``` ### VS Code `.vscode/mcp.json`: ```json { "servers": { "minerva": { "type": "stdio", "command": "npx", "args": ["-y", "minerva-mcp", "--stdio"] } } } ``` More, including read-only and app-only setups, in [`examples/`](examples/). On the first tool call the server prints a device-code prompt **on stderr**: open the link, enter the code, done. No app registration needed to try it — it uses the public Microsoft Graph PowerShell client by default. **Requirements:** Node.js 20+. For the PowerShell tools, [PowerShell 7+](https://aka.ms/powershell) and the modules you intend to use (`ExchangeOnlineManagement`, `PnP.PowerShell`, `MicrosoftTeams`). --- ## What you can ask > *"Which mailboxes have full access delegated to someone outside their department?"* > → `execute_exchange_online` with `Get-MailboxPermission`, no Graph equivalent exists. > *"Show me every conditional access policy that does not require MFA, and our Secure Score."* > → `generate_security_report` + `microsoft_graph_call`. > *"Create a shared mailbox for invoicing and give the finance team access."* > → classified as a write: the server describes the action and its consequences, > and waits for your confirmation before doing anything. --- ## Tools | Tool | What it does | Graph equivalent? | |---|---|:---:| | `microsoft_graph_call` | Any Microsoft Graph call (v1.0 or beta) | — | | `azure_arm_call` | Any Azure Resource Manager call | — | | `microsoft_search` | Unified Microsoft Search | — | | `get_tenant_context` | Organisation, licences, Secure Score | — | | `generate_security_report` | Tenant security posture | — | | `execute_exchange_online` | Exchange Online cmdlets | ❌ none | | `execute_pnp_sharepoint` | PnP PowerShell (SharePoint) cmdlets | ❌ none | | `execute_teams_admin` | Teams Admin cmdlets | ❌ none | | `execute_purview_compliance` | Purview / compliance cmdlets | ❌ none | Restrict the surface with `--tools=microsoft_graph_call,get_tenant_context`, or forbid every write with `--read-only`. --- ## Approval, and why it is not optional A model reads untrusted content — an email subject, a site name, a document — and decides which tool to call. Prompt injection is not hypothetical, and the tools here can delete mailboxes. So writes do not simply execute. Every action is classified `auto`, `confirmed` or `critical` from the HTTP verb and path (or the PowerShell verb). Anything above `auto` makes the server answer `input_required` under [MCP's Multi Round-Trip Requests](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr): your client shows what is about to happen and its consequences, you confirm, and only then does it run. The confirmation is a signed mandate (HMAC) bound to the caller, the method, and a digest of the arguments, valid five minutes. A mandate obtained to delete `/users/1` cannot be replayed on `/users/999`, and cannot be presented by anyone else. If your client does not support elicitation, nothing is silently executed: the server returns an error saying the action needs confirmation. --- ## Options Every flag has an environment variable equivalent. ### Transport | Flag | Env | Default | | |---|---|---|---| | `--stdio` | `MINERVA_TRANSPORT=stdio` | HTTP | JSON-RPC over stdin/stdout | | `--port=` | `MCP_SERVER_PORT` | `3001` | HTTP listen port | In stdio mode **all logs go to stderr** — stdout belongs to the protocol. ### Authentication | Flag | Env | | |---|---|---| | `--auth=` | `MINERVA_AUTH_MODE` | `bearer`, `device-code`, `client-secret` | | `--tenant=` | `MINERVA_TENANT_ID`, `AZURE_TENANT_ID` | Entra tenant | | `--client-id=` | `MINERVA_CLIENT_ID`, `AZURE_CLIENT_ID` | Application | | `--client-secret=` | `AZURE_CLIENT_SECRET` | App-only secret | Defaults: `bearer` over HTTP (the caller supplies the token), `device-code` over stdio, `client-secret` as soon as a secret is present. Each resource gets its own token: Graph, ARM, Exchange, Purview, SharePoint and Teams do not share an audience. ### Scope | Flag | Env | | |---|---|---| | `--read-only` | `MINERVA_READ_ONLY` | Refuse every write | | `--tools=` | `MINERVA_TOOLS` | Register only these tools | | `--ps=` | `MINERVA_PS_EXECUTOR` | `auto`, `redis`, `local`, `none` | | `--sharepoint-url=` | `MINERVA_SHAREPOINT_URL` | Required by the PnP tool locally | | `--allowed-origins=` | `MINERVA_ALLOWED_ORIGINS` | Browser origins allowed on `/mcp` | | `--redis-url=` | `REDIS_URL` | Queue for an external PowerShell runner | | — | `MINERVA_MRTR_SECRET` | Signing key for approval mandates | | — | `MINERVA_PWSH_PATH` | Path to `pwsh` | | — | `MINERVA_REQUIRE_VERIFIED_TOKENS` | Refuse tokens whose signature cannot be verified | `--read-only` reuses the same risk classifier as the approval gate, so it covers PowerShell cmdlets too, not just HTTP verbs. --- ## Protocol support Speaks **MCP 2026-07-28**, and still serves handshake-era clients until **2027-07-28**. | | | |---|---| | Stateless core (per-request `_meta`, no `initialize`, no `Mcp-Session-Id`) | ✅ | | `server/discover` | ✅ | | Multi Round-Trip Requests (`input_required`) | ✅ — this is the approval mechanism | | `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` headers, base64 sentinel | ✅ validated against the body | | `ttlMs` / `cacheScope` on lists | ✅ always `private` | | Version negotiation (`-32022` listing supported versions) | ✅ | | `Origin` validation (anti DNS-rebinding) | ✅ | | GET/DELETE on the MCP endpoint | ✅ `405`, removed by this revision | | Handshake era (`initialize`) | ✅ until 2027-07-28 | One endpoint serves both generations; the request body decides which. Both read the **same tool registry**, and a test compares them against a frozen contract so they cannot drift. --- ## Security Read [SECURITY.md](SECURITY.md) before deploying. The three things that matter: 1. **This server grants nothing.** It forwards a Microsoft token; Microsoft authorizes. A restricted token gives a restricted server. 2. **Graph tokens cannot be verified by a third party** — they carry a `nonce` header that only Graph can check. Their `tid`/`oid` claims are therefore self-reported. The server validates them structurally, flags them `verified: false`, and binds approval mandates to a **fingerprint of the token itself** rather than to the claims alone. A token that *should* have verified and did not is rejected. 3. **Local PowerShell mode isolates nothing.** Cmdlets run with the current user's rights. Do not expose it to a caller you do not trust. Cmdlet names and parameters never reach the script as code: they travel through environment variables, the name is validated against a strict `Verb-Noun` pattern, and the call goes through `Get-Command` plus the call operator. Report a vulnerability privately — see [SECURITY.md](SECURITY.md). --- ## What this does not do - **No process isolation** in local mode. - **No multi-tenant brokering** — one server, one identity. - **No signed audit ledger.** Audit events are JSON on stderr, not a signed, offline-verifiable record. Those three belong to [Minerva-IA](https://github.com/HoussemMak), the commercial platform this server was extracted from. Everything in this repository works without it. --- ## Development ```bash npm install npm run dev:stdio # stdio, hot reload npm run verify # typecheck + tests + secret scan npm run build && node scripts/smoke-stdio.mjs ``` Tests need no credentials and touch no network. Pull requests are closed until 2027-07-31 — see [CONTRIBUTING.md](CONTRIBUTING.md) for why, and for what is welcome in the meantime (bug reports very much are). ## Licence MIT — see [LICENSE](LICENSE).