ms-todo-mcp
by dcarrascosa
README.md
# Microsoft To Do MCP server
An [MCP](https://modelcontextprotocol.io) server that lets an LLM client (Claude
Desktop, Cowork, etc.) read and manage your **Microsoft To Do** tasks through the
**Microsoft Graph** To Do API.
It exposes seven tools:
| Tool | What it does | Write? |
|------|--------------|:------:|
| `todo_list_lists` | List your task lists | – |
| `todo_list_tasks` | List tasks in a list (by id or name), optional status filter | – |
| `todo_create_task` | Create a task (title, note, due date, importance) | ✓ |
| `todo_update_task` | Update title / note / due date / importance / status | ✓ |
| `todo_complete_task` | Mark a task completed | ✓ |
| `todo_delete_task` | Delete a task (permanent) | ✓ |
| `todo_create_list` | Create a new task list | ✓ |
## How auth works
Microsoft Graph **does not support application-only (daemon) permissions for
creating or updating To Do tasks** — only **delegated** permissions work. This
server therefore signs in as *you* using the OAuth2 **device-code flow** and
caches the resulting access + refresh tokens on disk, so you only sign in once.
> The token cache contains live credentials. It is written to
> `~/.ms_todo_mcp/token_cache.json` by default and is **git-ignored**. Never
> commit it.
## 1. Register an app in Microsoft Entra ID
1. [Entra admin center](https://entra.microsoft.com) → **App registrations** → **New registration**.
2. Name it e.g. `ms-todo-mcp`. For *Supported account types* pick
**Accounts in this organizational directory only** (single tenant) unless you
need broader access.
3. No redirect URI is required for the device-code flow. Click **Register**.
4. **Authentication** → **Advanced settings** → set
**Allow public client flows** = **Yes** (required for device-code).
5. **API permissions** → **Add a permission** → **Microsoft Graph** →
**Delegated permissions** → add **`Tasks.ReadWrite`**. If your tenant requires
it, click **Grant admin consent**.
6. Copy the **Application (client) ID** and **Directory (tenant) ID** from the
app's *Overview* page.
## 2. Configure
```bash
cp .env.example .env
# edit .env and set MS_TODO_CLIENT_ID and MS_TODO_TENANT_ID
```
| Variable | Required | Notes |
|----------|:--------:|-------|
| `MS_TODO_CLIENT_ID` | ✓ | Application (client) ID |
| `MS_TODO_TENANT_ID` | ✓ for work/school | Tenant ID/domain, or `organizations` / `common` |
| `MS_TODO_TOKEN_CACHE` | – | Override token-cache path |
| `MS_TODO_TIMEZONE` | – | IANA tz for due dates (default `UTC`) |
## 3. Install
Install the package into the **Python interpreter your MCP client will launch**.
By default that is plain `python` on your `PATH`. Because the client starts the
server from its own working directory, the simplest, path-free setup is to
install into that default interpreter — **not** a virtualenv:
```bash
pip install -e . # from the repo root (use `pip install .` for non-editable)
```
On Windows this also places an `ms-todo-mcp` launcher in your Python `Scripts\`
folder.
> **Want isolation instead of a global install?** Don't reach for a bare venv
> with the default config — `"command": "python"` won't see a venv package.
> Use **pipx** (`pipx install .`), which isolates the package *and* puts the
> `ms-todo-mcp` command on your `PATH`; then set `"command": "ms-todo-mcp"` in the
> config. (A manual venv also works, but only if the config points at that venv's
> `python.exe` by absolute path — see [`examples/README.md`](./examples/README.md).)
## 4. Sign in once
```bash
ms-todo-mcp login
```
Follow the printed instructions: open the URL, enter the code, sign in. The token
cache is then stored and reused.
## 5. Run
```bash
ms-todo-mcp # stdio transport (for local MCP clients)
ms-todo-mcp --http # streamable HTTP on :8000 (for remote use)
```
## 6. Add it to your MCP client
For a local **stdio** client (e.g. Claude Desktop / Cowork custom connector), add
an entry like the following. A ready-to-edit copy lives at
[`examples/claude_desktop_config.json`](./examples/claude_desktop_config.json) and
uses your **default Python** on `PATH` (no virtualenv path hardcoded):
```json
{
"mcpServers": {
"ms-todo": {
"command": "python",
"args": ["-m", "ms_todo_mcp"],
"env": {
"MS_TODO_CLIENT_ID": "<your-client-id>",
"MS_TODO_TENANT_ID": "<your-tenant-id>",
"MS_TODO_TIMEZONE": "Europe/Madrid"
}
}
}
}
```
Notes:
- The config file lives at `%APPDATA%\Claude\claude_desktop_config.json` (Windows)
or `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS).
Restart the app after editing it.
- For `python -m ms_todo_mcp` to resolve, install the package into that **default**
Python — run `pip install -e .` (or `pip install .`) **without** an active
virtualenv. If `python` isn't your launcher, use `python3` or the absolute
interpreter path. On Windows you can instead set `"command": "ms-todo-mcp"`
(the console script in your Python `Scripts` folder).
- **Run `ms-todo-mcp login` once first** so the token cache exists before the
client launches the server (the device-code prompt goes to stderr, not chat).
- Credentials live in `env` here because the repo `.env` is **not** read when the
client starts the server (its working directory isn't the project).
## Usage examples (what to ask your assistant)
- "List my To Do lists."
- "Show the unfinished tasks in my 'Tasks' list."
- "Add 'Send Microsoft invoices to Diego' to my Work list, due Friday, high importance."
- "Mark the task about UAT as completed."
## Development
```bash
pip install -e ".[dev]"
ruff check .
python -m py_compile src/ms_todo_mcp/*.py
```
## Project layout
```
ms-todo-mcp/
├── pyproject.toml
├── README.md
├── CONTRIBUTING.md
├── LICENSE # MIT
├── .env.example
├── .gitignore
├── examples/
│ ├── README.md
│ └── claude_desktop_config.json
└── src/
└── ms_todo_mcp/
├── __init__.py
├── __main__.py
├── auth.py
├── graph.py
└── server.py
```
## Releases
Releases are automated with
[release-please](https://github.com/googleapis/release-please-action). You do
**not** create tags or GitHub releases by hand.
The version is derived from [Conventional Commits](https://www.conventionalcommits.org/).
Because `main` is squash-merged, **the PR title is what counts** — prefix it
accordingly:
- `fix: ...` → patch bump (`0.1.0` → `0.1.1`)
- `feat: ...` → minor bump (`0.1.0` → `0.2.0`)
- `feat!: ...` or a `BREAKING CHANGE:` footer → major bump (`0.1.0` → `1.0.0`)
- `docs: ...`, `chore: ...`, `ci: ...`, `refactor: ...` → no release
Flow:
1. Merge PRs into `main` using Conventional Commit titles.
2. release-please opens (and keeps updating) a **release PR** titled
`chore: release X.Y.Z`, bumping the version in `pyproject.toml` and
`src/ms_todo_mcp/__init__.py` and updating `CHANGELOG.md`.
3. When you merge that release PR, release-please creates the `vX.Y.Z` git tag
and the GitHub Release with auto-generated notes.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues