Files
Spencer McGuire 2162aac515 Initial commit: Microsoft To Do for Claude (skill + .mcpb extension)
Claude Code skill (CLI over Microsoft Graph, delegated auth-code + PKCE) and a
Claude Desktop .mcpb extension sharing one dependency-free core client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:31:48 -06:00

6.5 KiB

Setup: Entra app registration for Microsoft To Do (delegated, browser auth-code)

Microsoft To Do has no app-only access — its Graph endpoints (/me/todo/...) only accept a signed-in user's token. So this skill authenticates as you (delegated) using the OAuth2 authorization-code + PKCE flow: login opens your system browser, you sign in, and the browser redirects back to a temporary local server. You create a public client app registration once, grant it the delegated To Do permission, and sign in once per machine. There is no client secret.

Why browser auth-code and not device-code? Many tenants block the device-code flow with a Conditional Access policy (error AADSTS53003). The browser flow runs in your real browser session, satisfying far more policies. If a policy requires a compliant/managed device, the Mac must be registered/enrolled (Company Portal/Intune) first — no flow can bypass that.

You do not need to be an admin if your tenant allows users to consent to the Tasks.ReadWrite delegated permission (it is a low-risk, user-consentable permission by default). If admin consent is required in your org, ask an admin to grant it once.

1. Register the application

  1. Go to https://entra.microsoft.comIdentityApplicationsApp registrationsNew registration.
  2. Name it something recognizable, e.g. claude-ms-todo.
  3. Supported account types:
    • work/school only → Accounts in this organizational directory only
    • also personal Microsoft accounts → ... and personal Microsoft accounts
  4. Leave Redirect URI blank for now — you'll add it in step 2.
  5. Register.

On the app's Overview page, copy:

  • Application (client) IDTODO_CLIENT_ID
  • Directory (tenant) IDTODO_TENANT_ID (optional; common also works)

2. Add a loopback redirect URI (public client)

  1. App → AuthenticationAdd a platformMobile and desktop applications.
  2. Under Custom redirect URIs, add both:
    • http://localhost
    • http://127.0.0.1 (Adding both avoids IPv4/IPv6 surprises. The skill redirects to http://127.0.0.1:<port> at runtime; Entra ignores the port for loopback redirects, so the OS-assigned port needs no registration.)
  3. Configure / Save.

This marks the app as a public client for that redirect — no client secret is needed; PKCE protects the code exchange. (You do not need "Allow public client flows" for auth-code+PKCE.)

3. Add the delegated Graph permission

App → API permissionsAdd a permissionMicrosoft GraphDelegated permissions. Add:

Permission Why
Tasks.ReadWrite Read/write the signed-in user's To Do tasks & lists
offline_access Issue a refresh token so sign-in persists (usually added automatically)
User.Read Identify the signed-in user in test (usually present by default)

If your tenant requires it, click Grant admin consent for <tenant>. Otherwise consent happens interactively during the first login.

Use Tasks.Read instead of Tasks.ReadWrite if you want read-only access. The skill's write commands will then return 403.

4. Give the skill the client id

Copy .env.example to .env in the skill directory and set TODO_CLIENT_ID (and TODO_TENANT_ID if not common). The CLI loads .env automatically, and .env is git-ignored. Alternatively, export them as environment variables.

5. Sign in (once)

node scripts/todo.mjs login

This opens your system browser to the Microsoft sign-in page (and prints the URL as a fallback if it can't auto-open). Sign in and approve; the browser redirects to a temporary local server and you'll see "Signed in ✓". The refresh token is cached to scripts/.token-cache.json (git-ignored, written with 0600 perms). The CLI refreshes silently after that — you won't be asked again until the refresh token expires or is revoked.

Run login on the machine whose browser you'll use; the temporary redirect server listens on 127.0.0.1, so the browser and the CLI must be on the same host (not over plain SSH without port forwarding).

6. Verify

node scripts/todo.mjs test

Expected: { "ok": true, "signedInAs": "you@contoso.com", "listCount": N, "lists": [...] }.

Troubleshooting:

  • AADSTS53003 (blocked by Conditional Access) — a CA policy blocked the sign-in. If it targets the device-code flow specifically, this browser flow already avoids it. If it requires a compliant/managed device (your device shows as Unregistered), enroll the Mac (Company Portal/Intune) or have an admin exclude this app. Use the sign-in log's Correlation Id to find the exact policy.
  • redirect_uri mismatch (AADSTS50011) — add both http://localhost and http://127.0.0.1 under the Mobile-and-desktop platform (step 2).
  • Browser redirect never returns / connection refused — something else holds the port, or you're on a remote/SSH session. Run login locally, or pin a port with TODO_REDIRECT_PORT and register http://127.0.0.1:<port>.
  • AADSTS65001 / consent error during sign-in — the org requires admin consent for Tasks.ReadWrite; have an admin grant it (step 3).
  • test returns 403 — the delegated Tasks.ReadWrite permission is missing or only Tasks.Read was granted.
  • Any command returns "needsLogin": true — the cached refresh token is gone or expired; run login again.
  • Personal Microsoft account won't work — set TODO_TENANT_ID=consumers (or common) and register the app for personal accounts in step 1.

Note: graduating to an MCP server

When you want this available across all sessions as native tools rather than a CLI, build a small MCP server that imports scripts/lib/graph.mjs:

import { MSTodoClient } from "./graph.mjs";
const client = new MSTodoClient({
  clientId: process.env.TODO_CLIENT_ID,
  tenantId: process.env.TODO_TENANT_ID,
  tokenCachePath: process.env.TODO_TOKEN_CACHE,
});
// Expose client.listTasks / getTask / createTask / updateTask / deleteTask
// (and the list/checklist methods) as MCP tools.

The client is dependency-free; its only state is the on-disk token cache. The one difference from an app-only server: you must run login once on the host to seed that cache before the server can call Graph. For a containerized HTTP transport, mount the token-cache file as a volume and guard the /mcp endpoint with a bearer token — the server acts as the single signed-in user.