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>
This commit is contained in:
144
.claude/skills/ms-todo/references/setup.md
Normal file
144
.claude/skills/ms-todo/references/setup.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 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 <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)
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user