Skip to main content
Glama
README.md
# Planner MCP

A remote Model Context Protocol server that lets Claude read, create, assign and
close Microsoft Planner tasks for a small team.

Claude's built in Microsoft 365 connector covers Outlook, Teams chat and
SharePoint, but not Planner tasks. This fills that gap.

Setup instructions are in [SETUP.md](./SETUP.md).

## What it does

Ask Claude to turn a brief into tasks and it does, assigned to the right people:

> Turn the Friday notes into Planner tasks. Aysyal takes the social channels,
> I take the tracking work, everything due next Friday in the To do bucket.

Ask it what is going on and it answers from live data:

> What is overdue in the marketing plan, and what is on Anton's plate this week?

## Tools

| Tool | Purpose |
| --- | --- |
| `whoami` | Which team member is connected, and which plans they can reach |
| `list_plans` | The plans this server can read and write |
| `list_buckets` | The columns in a plan |
| `list_tasks` | Tasks in a plan, filtered by assignee, bucket, status, overdue or due date |
| `my_tasks` | Open tasks for the connected person, across every plan |
| `get_task` | One task in full, including notes, checklist and links |
| `create_task` | One task, with assignees, due date, priority, notes, checklist, labels and a link |
| `create_tasks` | A batch, with shared defaults. This is the one to use for a brief |
| `update_task` | Change any field on an existing task |
| `assign_task` | Add people, or replace the whole assignee list |
| `unassign_task` | Take people off without deleting |
| `complete_task` | Mark done |
| `delete_task` | Remove permanently, and it insists on confirmation |
| `find_person` | Search the directory for the right email address |

## How it is built

Node 20 or newer, Express, and nothing else. One dependency total, so a deploy
is never held up by a transitive package.

### App only auth to Microsoft

The server talks to Microsoft Graph with the client credentials flow, using
`Tasks.ReadWrite.All`, `Group.Read.All` and `User.Read.All` application
permissions. There is no user token in the picture, so a password change or an
expired session never breaks it. The only credential with an expiry date is the
client secret, and that is on a 24 month clock.

### Nobody is hardcoded

Assignees arrive as email addresses and are resolved against Entra ID at call
time, with a one hour cache. The lookup falls back to searching `mail` and
`proxyAddresses` when an address is not somebody's sign in name, which is the
case for aliases. Adding a colleague to the team needs no code change and no
deploy.

Before assigning, the server checks whether the person is in the Microsoft 365
group behind the plan. Planner accepts assignments to outsiders without
complaining and then never shows them the task, so the server warns instead of
letting that pass silently.

### Its own OAuth server, so Connect asks for a password

Pressing Connect in Claude opens a login page served by this app. Type a work
email and the team password and you are in. Behind that page is a small OAuth
2.1 authorization server: dynamic client registration, PKCE with S256 required,
authorization code flow, refresh tokens.

Every piece of OAuth state is a signed token rather than a database row. Client
registrations, authorization codes, access tokens and refresh tokens are all
HMAC signed with `JWT_SECRET`. Three consequences worth knowing:

- No database to run or back up.
- A redeploy or a Railway restart does not disconnect anybody. This is covered by
  a test.
- Rotating `JWT_SECRET` disconnects everybody at once, which is the emergency
  brake.

Failed logins are rate limited to ten per address per fifteen minutes. Password
comparison is constant time.

### Planner's awkward corners, handled

- **Two ETags per task.** The task body and the task detail pane are separate
  objects with separate versions, and Planner rejects a write without the
  current one. Every update reads before it writes.
- **Basic plans only.** Planner Premium plans are not reachable through this
  API, so no Premium licence is needed or useful.
- **Dates.** A bare `2026-08-15` is stored at midday UTC, which keeps the date
  correct in both Amsterdam and Sofia.
- **Labels by name.** Pass `SEO` rather than `category1`. The server reads the
  plan's label names and translates.
- **Rate limits.** 429 and 503 are retried with backoff, honouring
  `Retry-After`.

## Tests

```bash
npm test
```

Starts a fake Microsoft Graph, spawns the real server, walks the entire OAuth
flow by hand, then connects with the official MCP client and calls every tool.
Ends with `91 checks passed`. It never touches the real tenant.

## Layout

```
src/
  index.js        Express app and routes
  config.js       Environment loading and validation
  auth.js         OAuth 2.1 authorization server and bearer verification
  jwt.js          HS256 signing, PKCE, constant time password comparison
  login-page.js   The password gate and the status page
  graph.js        Microsoft Graph client: tokens, retries, ETags
  people.js       Email address to Entra object ID, with alias fallback
  planner.js      Plans, buckets, tasks, details, labels
  tools.js        MCP tool definitions and output formatting
  mcp.js          JSON-RPC over the Streamable HTTP transport
test/
  mock-graph.js   Fake Microsoft Graph
  smoke-test.js   End to end test
```