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>
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
- Go to https://entra.microsoft.com → Identity → Applications → App registrations → New registration.
- Name it something recognizable, e.g.
claude-ms-todo. - Supported account types:
- work/school only → Accounts in this organizational directory only
- also personal Microsoft accounts → ... and personal Microsoft accounts
- Leave Redirect URI blank for now — you'll add it in step 2.
- Register.
On the app's Overview page, copy:
- Application (client) ID →
TODO_CLIENT_ID - Directory (tenant) ID →
TODO_TENANT_ID(optional;commonalso works)
2. Add a loopback redirect URI (public client)
- App → Authentication → Add a platform → Mobile and desktop applications.
- Under Custom redirect URIs, add both:
http://localhosthttp://127.0.0.1(Adding both avoids IPv4/IPv6 surprises. The skill redirects tohttp://127.0.0.1:<port>at runtime; Entra ignores the port for loopback redirects, so the OS-assigned port needs no registration.)
- 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 permissions → Add a permission → Microsoft Graph → Delegated 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.Readinstead ofTasks.ReadWriteif you want read-only access. The skill's write commands will then return403.
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
loginon the machine whose browser you'll use; the temporary redirect server listens on127.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_urimismatch (AADSTS50011) — add bothhttp://localhostandhttp://127.0.0.1under 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
loginlocally, or pin a port withTODO_REDIRECT_PORTand registerhttp://127.0.0.1:<port>. AADSTS65001/ consent error during sign-in — the org requires admin consent forTasks.ReadWrite; have an admin grant it (step 3).testreturns403— the delegatedTasks.ReadWritepermission is missing or onlyTasks.Readwas granted.- Any command returns
"needsLogin": true— the cached refresh token is gone or expired; runloginagain. - Personal Microsoft account won't work — set
TODO_TENANT_ID=consumers(orcommon) 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.