Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "mgraphctl",
"displayName": "mgraphctl",
"source": "./",
"version": "0.3.0",
"version": "0.4.0",
"description": "Work with Microsoft 365 — Outlook mail and calendar, Teams chats and channels, presence, meetings and transcripts, SharePoint, OneDrive, OneNote, Planner, To Do, people and org chart — through the Microsoft Graph API (Python CLI run with uv).",
"author": {
"name": "Sviatoslav Sviridov",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin.json",
"name": "mgraphctl",
"version": "0.3.0",
"version": "0.4.0",
"description": "Work with Microsoft 365 — Outlook mail and calendar, Teams chats and channels, presence, meetings and transcripts, SharePoint, OneDrive, OneNote, Planner, To Do, people and org chart — through the Microsoft Graph API (Python CLI run with uv).",
"author": {
"name": "Sviatoslav Sviridov",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mgraphctl",
"version": "0.3.0",
"version": "0.4.0",
"description": "Work with Microsoft 365 through the Microsoft Graph API using a bundled Python CLI run with uv.",
"author": {
"name": "Sviatoslav Sviridov",
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
# Changelog

## [0.4.0] — 2026-09-08

- An MCP server, `mgraphctl mcp serve`, exposing the CLI's verbs as tools to any MCP client. The
tool set is generated from the command tree, so a new verb is a new tool; `mcp tools` prints
what would be exposed. Needs the optional dependency `mgraphctl[mcp]`.
- `--capabilities mail,calendar,teams` picks the command groups to expose, because all 123 verbs
at once would cost the client a great deal of context. Default:
`core,mail,calendar,people,chats`. `all` covers everything but `api`.
- Read-only unless `--allow-write`. Tools carry the MCP behaviour annotations too, but those are
hints to the host, not the gate.
- `--json` has no tool equivalent: `output_format` chooses the compact table or the full payload
as structured content, and `output_file` writes the result under `--output-dir` and links it.
Results past `--max-inline-bytes` are written and linked regardless, so one wide fetch cannot
flood the client. Written files are readable back as MCP resources.
- Protocol revision `2026-07-28`, plus the earlier revisions the SDK negotiates. No deprecated
feature is implemented: no HTTP+SSE transport, no GET stream, no resumable streams. No
`Mcp-Session-Id` is minted in either era — the revision has no protocol sessions, and the
handshake revisions the SDK negotiates for older clients are served statelessly as well.
- `--transport http` binds loopback only and requires a bearer token and an allowed `Origin`.
The server acts as one signed-in user and cannot authenticate callers, so it does not pretend
to be an OAuth resource server; `stdio` is the recommended transport. README explains why.
- Every path a tool argument names is confined to `--output-dir`, including files a verb reads
(`--attach`, `--body-file`) rather than writes, and the server runs from inside that directory
so a verb's own default destination cannot land elsewhere. A downloaded file that is not text
reads back through `resources/read` as a blob.
- The `mgraphctl` shim adds `--extra mcp` when the noun being run is `mcp`, so the plugin can
start the server without the operator installing anything; every other verb's environment is
unchanged.
- The root flags carry into the server: `mgraphctl -dd mcp serve` logs every tool call's Graph
request to stderr, and `--tz` / `--beta` apply the same way they do on the command line.
- `render.emit` splits into `to_text`, `notes` and `to_json`, so a caller that does not own stdout
can render a result. The CLI's output is unchanged.
- `auth.app()` and `auth.save_cache()` take a lock. The CLI is single-threaded, but the MCP server
runs tool calls on worker threads, where a racing lazy build could bind an msal app to a cache
that is never written back — silently dropping a refreshed token.

## [0.3.0] — 2026-09-07

- Add a Codex plugin manifest and repository marketplace. Claude Code and Codex share the
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,67 @@ ${CLAUDE_PLUGIN_ROOT}/mgraphctl mail --help
${CLAUDE_PLUGIN_ROOT}/mgraphctl mail list --help
```

## MCP server

The same verbs are available to any MCP client, including hosts that cannot run a Claude Code
skill. Installed as a package it needs the optional dependency; run through the plugin's own
`mgraphctl` shim it does not, since the shim requests that extra for the `mcp` noun itself:

```bash
uv tool install "mgraphctl[mcp]" # or: pip install "mgraphctl[mcp]"
mgraphctl mcp tools --capabilities mail,calendar # what would be exposed, before wiring it up
mgraphctl mcp serve --capabilities mail,calendar # stdio, the recommended transport
```

```jsonc
// claude_desktop_config.json, or any other host's MCP config
{ "mcpServers": { "mgraphctl": {
"command": "mgraphctl",
"args": ["mcp", "serve", "--capabilities", "mail,calendar,teams"]
} } }
```

Three things are worth knowing before using it.

**Only what you ask for.** All 123 verbs as tools would cost the client a great deal of context,
so `--capabilities` names the command groups to expose. Omitted, it is
`core,mail,calendar,people,chats`; `all` is accepted and covers everything except `api`, the raw
Graph escape hatch, which is only ever exposed by naming it. `mcp tools` prints the resolved list.

**Read-only until told otherwise.** Verbs that change something appear only with `--allow-write`.
Tools also carry the MCP `readOnlyHint` / `destructiveHint` annotations, so a host can prompt
accordingly — but the annotations are hints, and `--allow-write` is the gate.

**Results stay small.** There is no `--json` tool argument. Every tool takes `output_format`
(`text`, the default compact table, or `json` for the full payload as structured content) and
`output_file`, which writes the result under `--output-dir` and returns a link instead of the
payload. Anything past `--max-inline-bytes` (25 KB) is written and linked whichever was asked, so
one wide fetch cannot flood the client. Written files are readable back as MCP resources. Every
path a tool argument names is confined to `--output-dir` — destinations like `--output`, and
equally files a verb *reads*, such as `--attach` and `--body-file` — and the server runs from
inside that directory, so a verb's own default destination lands there too.

The protocol revision is `2026-07-28`, plus the earlier ones the SDK negotiates. Deprecated
features — the HTTP+SSE transport, protocol sessions, the standalone GET stream, resumable
streams — are not implemented.

### A word on HTTP

`--transport http` exists, and it binds loopback only; anything else is a usage error rather than a
warning. The reason is worth stating plainly: **the server acts as exactly one user** — whoever's
sign-in is cached — and has no way to tell callers apart, so any reachable port is that person's
mailbox, calendar and files. It therefore also requires a bearer token (`MGRAPHCTL_MCP_TOKEN`, else
one is generated and printed at startup) and refuses any request carrying an `Origin` header that
`--allow-origin` does not list, which is the DNS-rebinding defence the transport spec requires of
local servers.

That token is a local shared secret, **not** OAuth, and the server deliberately advertises no
protected-resource metadata. Conforming to the MCP authorization spec means validating tokens
issued for this server as their audience; accepting the caller's Entra token instead is exactly the
token passthrough the spec prohibits, and doing it properly needs a second Entra app registered as
an API plus an on-behalf-of exchange — which requires a client secret that a locally installed CLI
cannot keep. Prefer `stdio`, where credentials come from the environment as the spec prescribes.

## Where things live

- Virtual environment: under `${CLAUDE_PLUGIN_DATA}` when Claude Code sets it, otherwise
Expand Down
1 change: 1 addition & 0 deletions VERSIONING.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ summarises it.
| `mgraphctl--v0.2.0` | 2026-09-06 | Python floor drops to 3.10, so the plugin runs under the Claude desktop app. |
| `mgraphctl--v0.2.1` | 2026-09-06 | Command reference split into one file per noun. |
| `mgraphctl--v0.3.0` | 2026-09-07 | Codex plugin manifest and repository marketplace added. |
| `mgraphctl--v0.4.0` | 2026-09-08 | MCP server: `mcp serve` and `mcp tools`, with capability groups, path confinement and the loopback HTTP transport. |
Loading