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

145 lines
6.5 KiB
Markdown

# 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.com> → **Identity****Applications**
**App registrations****New 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) ID** → `TODO_CLIENT_ID`
- **Directory (tenant) ID** → `TODO_TENANT_ID` (optional; `common` also works)
## 2. Add a loopback redirect URI (public client)
1. App → **Authentication****Add a platform** → **Mobile 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 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 &lt;tenant&gt;**.
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)
```bash
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
```bash
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`:
```js
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.