A Cloudflare Worker for Microsoft To Do and Claude AI

My latest project, mstodo-mcp-cloudflare, is now public. It‘s a Cloudflare Worker that connects Claude.ai to your Microsoft To Do account over MCP. You can ask Claude to find, create, update, complete, and search tasks across all your lists in plain language without leaving the conversation. Open source, MIT licensed, and runs on the Cloudflare free tier for single-user installs with a normal task count, with great performance for Paid Worker tier if you need it. Allows for sub-list item control, multi-attachment deduplicated uploads, and custom linked items by content (define a ticket number regex and the system adds a related ticket link for tasks mentioning a ticket number, configurable via MCP conversation).
This follows the Obsidian MCP and link resolver Workers I released two days ago, using the same core pattern: Cloudflare Worker, Durable Object for stateful storage, OAuth for access control, all serverless and scaling to zero.
Why I built it
I rely on Microsoft To Do for task management across all my devices. My Day resets every night, the sync is solid everywhere I work, and Outlook flagging integration sounded cool, but I abuse flags to the point of useless in a task app so I turned them off in To Do :-) (Don't worry, it still works here.)
I have more lists than most people would consider reasonable. Task lists, reference lists, and project-specific lists. That spread is both the strength and the weak point of the setup. Finding something across all of them required either opening To Do directly and searching, or remembering which list it was in.
The thing I wanted was conversational access to the whole picture. Not an AI embedded inside a task app behind whatever narrow interface the vendor provides, but an AI that could reach into the task app from a conversation, query across all the lists at once, and create tasks with context from whatever I was already discussing.
Before building this, I looked at what already existed. n8n is powerful, I use it a lot, it has Microsoft Graph integration (even with To Do directly!) and can wire up MCP-style operations for AI clients. The problem is coverage. The Microsoft To Do operations available through n8n's connector cover a subset of what the Graph API supports, and nothing is cached. A cross-list query through n8n means walking every list and fetching its tasks from the Graph API in sequence. This is fine for a single-list automation triggered manually, but slow enough to be painful for "show me everything due this week across all my lists" asked as a casual conversational query. With Microsoft Graph rate limits in play, bulk reads on a large account get uncomfortable fast. And n8n can build MCP servers easily but has some limitations on function structure that limit its capabilities here. I built it, used it periodically for 6 months, but always wanted something more complete.
Building the Obsidian MCP Worker made the right architecture clear. A local mirror in a Durable Object's SQLite database, kept current by delta sync, handles both problems: it exposes the full API surface and makes cross-list queries instant regardless of task count. Applying the same pattern to To Do was the natural next step.
What I am using it for
The most immediate change to my daily workflow is…honestly unknown, I just built it! Of course I can ask Claude to pull all incomplete tasks due this week across all my lists, grouped by list type, and get an actual picture rather than scrolling through 20-odd lists one at a time. That query used to require opening To Do and filtering repeatedly, which I mostly did not bother to do. I can ask it to take a list of follow-up items in Obsidian and convert them to Microsoft To Do tasks, or I can take a reference item in To Do and have Claude research it with the results saved in an Obsidian note.
The other thing that comes up constantly is task creation from context. When something comes up mid-conversation (a follow-up to schedule, something to note about a project, a call to make, an ongoing Obsidian note I'm creating or editing) asking Claude to add a task is faster than switching apps, finding the right list, and typing. Because the server holds the full list roster with aliases I have configured, Claude should usually route the task correctly from a description without needing an exact list name.
Cross-list search is less dramatic but I use it more than expected. I frequently remember writing something down without remembering where. Full-text search across all task titles and notes, answered from the local SQLite FTS5 index, comes back fast without hitting the Graph API at all.
The other thing To Do is bad at surfacing in the app is completed tasks. You can see them, but only per-list and with limited sorting capability. Because all completed tasks are synchronized here, reporting with any type of filters should be straightforward. And I’m contemplating (it’s not here yet) adding a REST API to make this reporting even easier from n8n or other systems (I’m doing this now in n8n for a periodic “recently completed tasks” email summary, but with limitations this database copy would remove).
How it works
Claude.ai connects to the Worker over remote MCP, authenticated through an OAuth 2.1 flow the Worker brokers against Microsoft Entra (Azure's identity layer). The first connection runs a standard browser OAuth flow where you authorize with your Microsoft account; the Worker stores the resulting refresh token in Cloudflare KV and handles refresh from then on. Claude reconnects through the Worker without re-authorization.
The central component is a TodoIndex Durable Object. Cloudflare Durable Objects are stateful compute primitives: strongly consistent, single-instance, with embedded SQLite storage. This one holds a complete local copy of all your lists and tasks, plus an FTS5 full-text index over task titles and note content. Rather than calling the Microsoft Graph API on every tool request, the Worker reads cross-list queries and search results from this local mirror.
A */15 cron job runs delta sync on a 15-minute schedule. The Graph API supports delta queries: after the first full baseline, each sync returns only changes since the last cursor. Most sync cycles are small: a few completed tasks, a few new ones. The first baseline after deploy does a full walk and can take several cron cycles on a large account; steady-state sync is cheap. Write operations (create, update, delete, complete) call Graph directly and write back to the local mirror in the same request, so changes show up immediately without waiting for the next cron.
Small, infrequently-changed state lives in Cloudflare KV: OAuth tokens, the owner-identity record, and configuration blobs. The task corpus lives in the Durable Object's SQLite rather than KV. KV handles configuration-scale reads well; it is not designed for indexed queries across thousands of rows. SQLite in a Durable Object is.
The server is single-user by design. After first authorization, any OAuth completion against a different Microsoft identity triggers an automatic wipe of the task mirror, per-identity aliases, and stored tokens before the new identity is saved. The wipe runs before new tokens are stored, fail-closed: a mid-wipe failure causes the next authorization to re-trigger it rather than leaving a half-mixed state. Everyone who is not the registered owner is rejected before reaching any tools.
Tools
The tool surface covers CRUD on lists and tasks, checklist items (sub-tasks) within tasks, linked resources (URLs attached to a task), and attachments. The cross-list layer is where the implementation cost gets justified.
query_tasks filters by list, status, date range, importance, and whether a task has a checklist. The types and exclude_types parameters filter by list classification (explained below), so you can request all incomplete todo-type tasks due this week without naming every list. Results come from the local SQLite mirror, paginated.
search_tasks runs FTS5 full-text search over task titles and notes with the same filter set. SQLite's FTS5 engine supports prefix matching, phrase queries, and column weighting. exclude_types:["excluded"] is a useful default filter that drops noise lists (flagged-email lists, reference-only lists) from results without deleting anything.
Aggregation tools: get_pending_across_lists surfaces tasks that have been sitting incomplete for a while; get_recently_completed gives you the last N completed items across all lists, useful for writing a quick status summary. find_task_list resolves a name or alias to a list ID for use in other calls.
Operational tools: whoami returns the current Microsoft account identity, sync_status shows last sync time, list counts, and delta cursor state per resource, and resync forces an immediate sync outside the cron schedule.
List classification
With enough lists in play, it is useful to classify them so queries can target a category instead of naming every list. The config:lists configuration in KV accepts an ordered set of regex patterns matched against list display names. First match wins, assigning one of: todo, reference, excluded, or leaving the list unclassified if nothing matches. This can be configured via conversation with AI, along with one or more list name aliases to make it simpler to mention the correct list in conversation despite any display name in the To Do app.
A todo list is an active task list. A reference list holds information without action dates. An excluded list is indexed but kept out of type-filtered queries by default -- useful for lists you do not want appearing in "all my active tasks" but still want searchable. The flaggedEmails well-known list is skipped during sync by default regardless of your patterns, since it can be large and you may not use it as a task list; you can turn sync on for it explicitly. (I found nearly three-quarters of my nearly 10,000 existing tasks that synchronized were flagged emails!)
Aliases map a short readable name to a Graph list ID, so you can refer to "inbox" or "work" anywhere a list identifier is accepted. Both patterns and aliases are set conversationally through set_list_config and persist in KV. Aliases clear automatically on a Microsoft account switch, since Graph list IDs are per-account.
The no_sync option in the list config excludes specific lists from delta sync entirely. They remain readable on demand via list_tasks but their tasks are not indexed and do not appear in cross-list queries. This is useful for very large lists you rarely query through AI, and it reduces Durable Object write counts on every sync cycle, which matters on the free plan if your account is large.
Attachment upload
Inline file upload through MCP tool calls does not work in practice. Claude's per-call argument limit caps tool input at a few KB, nowhere near enough to transmit a file. The binding constraint is the MCP transport, not Microsoft Graph's attachment limits. I confirmed this while building the Obsidian MCP server, and the same web-upload solution applies here, ported and adapted for the Graph attachment APIs.
create_upload_link mints a short-lived, single-use link that you open in a browser. The link is a 32-byte cryptographically random ID stored server-side in KV with a configurable TTL (default 15 minutes, max 30). The ID maps to the target task; holding it authorizes one upload to exactly that task. When you open the link and select a file, the bytes go browser to Worker to Microsoft Graph, the model never sees them. Files up to 3 MB attach inline; files up to 25 MB go through a chunked Graph upload session. Duplicate detection by content hash skips files already attached to the task.
There is no signing key or shared secret to configure. The 32-byte random ID is the capability token. To enable the feature, set SERVICE_BASE_URL in wrangler.jsonc to the public URL of your Worker. Without that variable, create_upload_link returns upload_disabled.
Deploy it
Setup requires three things: a Microsoft Entra app registration (one-time, free, fully walked through in the deployment guide, should take about 10 minutes of clicking in the Azure portal), a Cloudflare account (the free tier handles normal task counts; Workers Paid is worth considering once you reach thousands of tasks, since the first full sync writes roughly two Durable Object rows per task and the free tier caps daily writes account-wide), and Node 18+ with the Wrangler CLI. The full step-by-step walkthrough (Entra registration, KV namespace creation, secrets configuration, wrangler setup, and first authorization) is in DEPLOYMENT.md.
The repo is at dszp/mstodo-mcp-cloudflare.