Skip to main content
Glama

ms-to-do-mcp

An MCP server for Microsoft To Do, in two deployments sharing the same eleven tools and the same Azure app registration:

  • Local Docker (this README, Python) — streamable HTTP on http://127.0.0.1:8787/mcp, unauthenticated, loopback only. Any local MCP client — Claude Code, Codex, Claude Desktop — points at that one URL and shares the same container and the same Microsoft sign-in.

  • Remote Cloudflare Worker (worker/, TypeScript) — for hosted clients like ChatGPT web that cannot reach 127.0.0.1. Fronted by OAuth 2.1 + PKCE, with Microsoft as the identity provider.

Both talk to Microsoft Graph as a public client against the /consumers tenant, with the Tasks.ReadWrite scope. The local version signs in with the device-code flow; the Worker with the auth-code flow.

Tools

Tool

list_task_lists

every list, with its active-task count

list_tasks

tasks across one or all lists, sorted by due date; include_completed, due_before, limit

get_task

one task in full, including checklist steps

search_tasks

substring match over titles and notes, all lists

list_checklist_items

a task's steps

create_task

new task with due, reminder, notes, importance

update_task

change any field; empty string clears a due date or reminder

complete_task

mark done (recurring tasks roll forward)

delete_task

permanent — annotated destructiveHint

create_task_list

new list

add_checklist_item

add a step to a task

Lists and tasks are addressed by display name / title, not by Graph's opaque ids: exact match first, then a unique case-insensitive substring, and an error naming the candidates when a reference is ambiguous. Raw ids are accepted too.

Dates use YYYY-MM-DD or YYYY-MM-DD HH:MM in TODO_TIMEZONE. To Do stores a due date as a plain date, so a time passed in due is dropped by Microsoft — use reminder for a time-of-day alert.

Related MCP server: Todoist MCP Server

Setup (local Docker)

colima start                          # or Docker Desktop — the daemon must be up
docker compose run --rm ms-todo auth  # first run only — Microsoft sign-in (see below)
docker compose up -d
docker compose logs -f ms-todo

Everything else is in compose.yaml. The port is published as 127.0.0.1:8787 on purpose: the MCP endpoint is unauthenticated, so anything that can reach it can drive the To Do account. Never publish it as bare 8787.

For the remote deployment, follow worker/README.md instead — Cloudflare login, two KV namespaces, an Azure redirect URI, and npx wrangler deploy.

The sign-in

The Microsoft token lives at ~/.ms-todo-mcp/token.json (dir 700, file 600), outside this repo, bind-mounted into the container at /data. The container refreshes it in place and persists the rotated refresh token; never copy this file elsewhere, since two copies race each other's rotation and both eventually break.

The same auth command handles both the first sign-in and the re-sign-in Microsoft occasionally forces:

docker compose run --rm ms-todo auth

It prints a code to enter at microsoft.com/devicelogin on the host — no redirect URI or in-container browser needed, which is what makes this containerise cleanly.

Client registration

Claude Code:

claude mcp add --transport http --scope user ms-todo http://127.0.0.1:8787/mcp

Codex — in ~/.codex/config.toml:

[mcp_servers.microsoft_todo]
url = "http://127.0.0.1:8787/mcp"
startup_timeout_sec = 20
tool_timeout_sec = 120

For the Worker, register https://<worker>/mcp instead — the client then runs the OAuth flow on first use (details in worker/README.md).

Checking it works

# auth + Graph reachability, independent of MCP
docker compose run --rm --entrypoint python ms-todo -c \
  "from ms_todo_mcp.graph import get_all; print(len(get_all('me/todo/lists')), 'lists')"

# MCP handshake
curl -sS http://127.0.0.1:8787/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

A 404 means streamable_http_path is wrong; a 421 means the MCP_ALLOWED_HOSTS allowlist (DNS-rebinding protection) doesn't cover the Host header you sent.

Layout

src/ms_todo_mcp/     the local Docker server (Python)
├── graph.py         token lifecycle + Graph HTTP (stdlib urllib only, no deps)
├── auth.py          device-code sign-in
├── server.py        MCPServer + the 11 tools
└── __main__.py      `serve` (default) | `auth`

worker/              the remote Cloudflare Worker (TypeScript) — see worker/README.md

Built against mcp 2.x (MCPServer, not the removed 1.x FastMCP).

Related MCP Connectors

Related MCP Servers