Skip to main content
Glama
dragosh29

Plannr MCP Server

by dragosh29
README.md
# Plannr MCP server

An [MCP](https://modelcontextprotocol.io) server that lets Claude, ChatGPT and other MCP clients work with a financial adviser's [Plannr](https://plannrcrm.com) CRM: clients, households, reviews due, tasks, cases, plans and notes, and (when enabled) creating tasks and notes. It is built from Plannr's public documentation: the OpenAPI 3.0.3 spec "Plannr API Documentation" at `apidocs.plannrcrm.com/source.json` and the API guide at `api-how-to.plannrcrm.com` (authentication, required headers, pagination, rate limits).

Once it's connected, an adviser or administrator can ask things like:

- "Whose annual review is due in the next month, and who is their adviser?"
- "What's open for the Evans family: tasks, cases and plans?"
- "Which of Sam Evans's pensions are under advice, and with which providers?"
- "What did we last discuss with Sam? Show me the call notes since September."
- With writes enabled: "Create a high-priority task to call Sam about drawdown on Friday, and log a meeting note on his record."

## Tools

| Tool | What it does | API calls |
|---|---|---|
| `list_logins` | The logins of the user behind the token: the account each acts as (UUID, name, type, role) and the firm. Shows which account the server acts for: `PLANNR_ACCOUNT_UUID`, or the one chosen from the same logins response by the rule below, or a note that none could be chosen. | `GET /api/v1/logins` |
| `search_clients` | Accounts (clients by default) by name, role, prospect flag, status, tags, assigned adviser or household, with sort. | `GET /api/v1/account` |
| `list_reviews_due` | Clients whose next review date falls in a window (today to +30 days by default), soonest first. | `GET /api/v1/account` with `filter[next_review_date]=>=FROM,<=TO` and `sort=next_review_date` |
| `get_client` | One account: adviser, administrator, paraplanner, owners, groups, tags, households, service level, review and agreement dates, custom field names. | `GET /api/v1/account/{uuid}` |
| `list_households` | Circles (households) with members by name, portal access and engagement rating. | `GET /api/v1/circles` |
| `get_household` | One circle with members, groups and last portal login. | `GET /api/v1/circles/{uuid}` |
| `list_tasks` | Tasks by client, assignee, related plan or case, priority, due-date range, open only, name, status, with sort; each with its author and who completed it. | `GET /api/v1/task` with `include=author,completed_by` |
| `get_task` | One task with its description, workflow and custom field names. | `GET /api/v1/task/{uuid}` |
| `list_task_statuses` | The firm's task statuses in board order (needed by `create_task`). | `GET /api/v1/task-status` |
| `list_cases` | Cases by client, household, employee, progress, review or completion dates, status or type, with participants by name. Sorting by value is not offered. | `GET /api/v1/cases` |
| `get_case` | One case with its linked plans and custom field names. | `GET /api/v1/cases/{uuid}` |
| `list_plans` | Plans by client, household, type, abstract type, status, provider, name or under-advice flag: type, provider, status, owners, review date. Sorting by value is not offered. | `GET /api/v1/plans` |
| `get_plan` | One plan with seller, sub-accounts by name and custom field names. | `GET /api/v1/plans/{uuid}` |
| `list_client_notes` | A client's notes, including notes on their plans, cases, tasks and risks, newest first, by type, date range, text or what they are attached to. | `GET /api/v1/client/{client_uuid}/all-notes` |
| `create_task` | Creates a task on a client, employee, case or plan. Only registered when writes are enabled. | `POST /api/v1/task` |
| `add_note` | Adds a call, note, meeting or email note to an account, household, case, plan or task, not visible to clients unless asked. Only registered when writes are enabled. | `POST /api/v1/note` |

Not covered on purpose: everything else in an 875-operation API, including contact details and addresses endpoints, documents and files, fact finds, valuations, holdings, transactions, charges and fees, bank feeds, messages, webhooks, and every update and delete. Searching clients by email, phone number, National Insurance number or date of birth (all documented filters) is not offered either.

## Setup

Requires Node 18 or later.

```bash
npm install
npm run build
```

You need a personal access token: in Plannr, go to **Settings > Account Details** and scroll to **Personal Access Tokens**. The guide recommends these for personal use and for agencies working on behalf of a Plannr customer. Plannr's OAuth2 authorization code flow (for apps used by many firms; client credentials are issued by emailing integrations@plannrcrm.com) is out of scope for this local version; see Going to production.

Most endpoints need the account to act for in an `X-PLANNR-ACCOUNT-UUID` header. Set `PLANNR_ACCOUNT_UUID` to the `account.uuid` of your employee login (the `list_logins` tool shows them). If it is not set, the server calls `GET /api/v1/logins` once and uses the response's `preferred_account_uuid` if there is one, otherwise your only employee login; if you have several employee logins it refuses and lists them so you can choose.

**Claude Desktop:** add this to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "plannr": {
      "command": "node",
      "args": ["/absolute/path/to/plannr-mcp/dist/index.js"],
      "env": { "PLANNR_ACCESS_TOKEN": "your-personal-access-token", "PLANNR_ACCOUNT_UUID": "your-employee-account-uuid" }
    }
  }
}
```

**Claude Code:**

```bash
claude mcp add plannr -e PLANNR_ACCESS_TOKEN=your-personal-access-token -e PLANNR_ACCOUNT_UUID=your-employee-account-uuid -- node /absolute/path/to/plannr-mcp/dist/index.js
```

| Variable | Required | Meaning |
|---|---|---|
| `PLANNR_ACCESS_TOKEN` | yes | Your personal access token, sent as `Authorization: Bearer …`. A value pasted with its `Bearer ` prefix is accepted. |
| `PLANNR_ACCOUNT_UUID` | no | The account to act for, sent as `X-PLANNR-ACCOUNT-UUID`. Resolved from `GET /api/v1/logins` when not set (see above). |
| `PLANNR_ALLOW_WRITES` | no | `true` to register `create_task` and `add_note`. Off by default. |
| `PLANNR_BASE_URL` | no | Defaults to `https://api.plannrcrm.com`. Used by the tests. |
| `PLANNR_TOOL_BUDGET_S` | no | Time budget per tool call in seconds (default 45; see Safety defaults). Lowered by the tests. |

Every request carries `Accept: application/json` and `Content-Type: application/json`, which the guide lists as required, and every request except `GET /api/v1/logins` carries `X-PLANNR-ACCOUNT-UUID`.

## Safety defaults

- Read-only unless `PLANNR_ALLOW_WRITES=true`. Read tools carry the MCP `readOnlyHint` annotation; the two write tools are marked not read-only, not destructive and not idempotent. There is no update or delete tool.
- Clients and everyone else are identified by UUID and name by default. Only with `include_contact_details=true` does a tool return email addresses (of accounts, logins, members, assignees, authors and participants), the primary email and phone number, custom field values (only their names are shown by default, because a firm can record income, health or anything else in them), third-party references (platform and provider references) and policy, proposal, linked-policy and sub-account policy numbers.
- Never read from the records: money fields (plan values, valuations, case values, benefit amounts; see Status), bank details, National Insurance numbers, dates of birth and addresses. Every output is built field by field from a list of known fields, so a field of that kind is not copied even if a response carries it (the test suite serves records with `date_of_birth`, `ni_number`, `bank_accounts`, an address, an income and money objects and checks none of them comes back, with or without `include_contact_details`). The endpoints that hold contact details, addresses, documents and bank data are never called (the suite asserts the exact set of endpoints used). Sorting by value, which the API documents for cases and plans, is not offered, because the order would reveal the relative values the server otherwise withholds. No file is ever downloaded.
- Free text (client, household, task, case, plan and sub-account names; tag, group, status, type, provider, service level, workflow and custom field names; firm names; task descriptions; note contents; the error messages Plannr returns) is redacted by pattern. Always, with or without `include_contact_details`: a sort code with an account number, a sort code or account number introduced as such ("sort code", "acct", "account no."), a GB IBAN (`[bank details redacted]`), a 13 to 19 digit number that passes the Luhn check (`[card number redacted]`), a National Insurance number in the `QQ 12 34 56 C` shape (`[NI number redacted]`) and a date written after "DOB", "born", "date of birth" or "birthday" (`[date of birth redacted]`). By default, and not with `include_contact_details`: email addresses (`[email redacted]`), phone-number-like sequences (`[phone redacted]`) and UK postcodes (`[postcode redacted]`). Custom field values, returned only on request, go through the always-on part.
- These are heuristics. The phone match covers international numbers written with `+` or `00` (including `+44 (0)7700 …`), UK numbers with a bracketed area code, and UK-style `0…` numbers of 9 to 11 digits with spaces, dots or hyphens between groups; other digit strings starting with `0` are redacted too, and so is any other 13 to 19 digit string that happens to pass the Luhn check (about one in ten), while UUIDs and timestamps are left alone. A bare date of birth, a bare six- or eight-digit number, a street address, an amount such as "salary £85,000" and a health detail are not caught and are returned as written, as is the rest of a note, since calling the notes tool is itself the request for notes. HTML tags in notes (such as the `<span>` of an @mention) are stripped.
- IDs are checked before any call: every ID must be a UUID. Dates must be real calendar dates in `YYYY-MM-DD` form, a review window must not end before it starts, and a tag, status, provider or plan name may not contain a comma (the API takes these as comma-separated lists).
- Rate limits: Plannr documents 500 requests per minute per user, an `X-Ratelimit-Remaining` header on every response and a 429 when the limit is exceeded, with the advice to back off; it does not mention `Retry-After`. Requests are spaced 150 ms apart (at most 400 a minute). A 429 is retried at most twice, waiting for `Retry-After` when present (in seconds or as an HTTP-date) and 2 s then 4 s when absent. Each wait is capped at 10 seconds; if Plannr asks for a longer wait the call gives up at once and says how long. Because one tool call can make several requests (the account lookup, then up to 10 pages of a list), every tool call also has a 45 s time budget (`PLANNR_TOOL_BUDGET_S`), under the MCP SDK's default 60 s request timeout: a retry wait that would end past it is not started and no further page is requested, the call fails at once saying so, and nothing is sent to Plannr after the client has given up. The server does not read `X-Ratelimit-Remaining`.
- 502, 503 and 504 are retried the same way for `GET` only; when all three attempts fail the error says the service may be unavailable, without the gateway's page. `POST /api/v1/task` and `POST /api/v1/note` are never retried after a gateway error, because the task or note may already exist; the error says to check `list_tasks` or `list_client_notes` first.
- A 200 whose body is not JSON (a proxy or login page in the way) is reported as an error naming `PLANNR_BASE_URL`, described by content type and size without quoting it. From error responses only Plannr's documented `message` and `errors` fields are passed on, redacted as above, and the access token is scrubbed from every message.
- Pagination follows the `links.next` URL Plannr returns. The server never builds a page number itself, re-adds any of its own filters the link leaves out, and refuses to follow a link to another host or another endpoint.
- A rejected token (401) produces a message that says which variable to fix and where tokens are created; a 403 names the account in use and points to `PLANNR_ACCOUNT_UUID` and `list_logins`; a 422 is passed on with Plannr's field errors.

## Tests

```bash
npm test
```

The test suite:

1. Validates every fixture record against the component schemas in Plannr's published OpenAPI spec (`LoginResource`, `AccountResource`, `CircleResource`, `TaskStatusResource`, `TaskResource`, `IndexTaskResource`, `CasesResource`, `Plans_PlanResource`, `NoteResource`) with Ajv and `ajv-formats`. Because the schemas set no `additionalProperties: false` and mark almost nothing as required, every fixture is also walked key by key (following `$ref`, `allOf` and `items`) and a key the schema does not declare fails the check. Negative controls check that a schema still rejects wrong types and that the walk reports undeclared keys at any depth. The spec is downloaded from `apidocs.plannrcrm.com/source.json` to `spec.json` on the first run.
2. Starts a local mock of the API that serves those fixtures with the documented filters (every filter the server sends is applied, except `filter[is_prospect]` and `filter[status]` on `/api/v1/account`, since `AccountResource` declares neither a prospect flag nor a status; their pass-through is still asserted), the include-only relationships of the task index (`author` and `completed_by` only when included), `per_page` and the Links/Meta envelope the guide describes (on `/api/v1/task` the next link carries only the page number, elsewhere it keeps the query), Bearer auth with the spec's 401 body, a 403 for a missing or unknown `X-PLANNR-ACCOUNT-UUID`, 404s, 422s in the `Plannr_ValidationError` shape, an `X-Ratelimit-Remaining` header, a one-off 429 with `Retry-After` on `GET /api/v1/task-status`, and injected failures or replacement records on any endpoint (including a 429 on every other request, so that each page of a list is refused once). The mock's list, detail, created-task and error responses are validated against the response schemas the spec gives for each operation.
3. Starts the built server and drives it over stdio with the official MCP client: 31 checks covering tools/list and annotations, every read tool, pagination across two pages to the point where `links.next` is null (and a `max_results` cut reported as incomplete), a next link without the filters (they are re-added) and requests on one call spaced at least 140 ms apart, every documented filter each tool offers passed through exactly, the reviews-due operator filter, redaction of emails, phone numbers, custom field values, references and policy numbers by default and their return on request, contact details typed into client, household, tag, group, service level and custom field names and descriptions redacted, a fact-find note whose NI number, date of birth, bank details and card number are redacted in both modes and its postcode by default only, planted records served in place of every read (and of the created task and note, and of a 422) with those details and contact details in every free-text field and undeclared `date_of_birth`, `ni_number`, `bank_accounts`, address, income and money fields, none of which comes back, HTML stripped from notes, no money fields in plan and case output, a detail answer and a created task or note wrapped in `{"data": …}` unwrapped, the `POST /api/v1/task` and `POST /api/v1/note` bodies validated against the spec's request schemas (`StoreTaskRequest`, `StoreNoteRequest`), a 422 passed on with field errors, an empty 204 on a note handled, writes absent with `PLANNR_ALLOW_WRITES` unset and set to `false`, bad UUIDs, dates, reversed review windows and comma-containing names rejected before any request, the 404 message, the 429 retry waiting for `Retry-After` in the seconds and HTTP-date forms and 2 s then 4 s without it, giving up after three attempts and at once above the cap, a 429 on `POST /api/v1/note` retried once, a 429 on the first `GET /api/v1/logins` with two calls at once resolving the account once, the per-call time budget (lowered to 3 s) stopping a list whose pages each get a 429 with a clean error and no later request, a 502 retried for `GET`, a `GET` failing three times with 503 reported without the HTML, a 502 on `POST /api/v1/task` and a 503 on `POST /api/v1/note` not retried, a non-JSON 200 reported as an error, next links to another host or another endpoint refused, an error message passed on with its email, phone number and the token redacted, the account resolved from `/api/v1/logins` (the only employee login, `preferred_account_uuid`, and a refusal listing the choices when ambiguous), `list_logins` showing the account it acts for with `PLANNR_ACCOUNT_UUID` unset (or that none was chosen), the 401 message for a wrong token (pasted with a `Bearer ` prefix, not doubled), the 403 message for a wrong account, and that every request used the Bearer token, the required headers, a documented method and path and only query parameters that operation documents (apart from the page number taken from Plannr's next link), with the exact set of 15 endpoints used.

## Status

This is a working prototype. It has **not yet been run against the live API**, because it was built without a Plannr account (no public trial or sandbox was found; personal access tokens come from customers' own accounts). Everything below is taken from the published documentation and should be confirmed on a real account:

- Pagination. The spec documents only `per_page` (default 15, max 500) and declares only `data` in its list responses; the guide describes a Links block with four page links and a Meta block (per page, total, current and last page) in a screenshot. The key names used here (`links.first/last/prev/next`, `meta.current_page/last_page/per_page/total`) are the Laravel convention that description matches. Whether `links.next` keeps the filters is not documented; both cases are handled. Whether `per_page=100` is accepted everywhere this server uses it.
- Get-by-UUID and create responses. The spec types them as the resource itself; if the live API wraps them in `{"data": …}` they are unwrapped.
- `POST /api/v1/note`: the spec documents no success response (only 401 and 422). The mock answers 201 with a `NoteResource`; the tool also accepts an empty answer.
- The account header: what status a missing or wrong `X-PLANNR-ACCOUNT-UUID` gets (the mock answers 403), whether `GET /api/v1/account/{uuid}` and the notes endpoints need it (the spec does not declare it there; the guide says every endpoint but the logins list does, so it is always sent), and where `preferred_account_uuid` sits in the `GET /api/v1/logins` response (the guide names it but it is not in the spec; the server looks at the top level and on each login).
- Filter encoding and semantics: booleans are sent as `true`/`false`; the operator filter `filter[next_review_date]=>=2026-10-01,<=2026-10-31` is sent URL-encoded; whether `filter[client_uuid]` on tasks includes tasks on the client's cases and plans (the mock matches tasks on the client only); whether `filter[status]` on accounts matches what the UI calls a client status (the `AccountResource` schema has no status field); which relationships the index routes return without an `include` (the mock returns only those included).
- Relationship shapes: the spec types a case's `participants` and `plans` as single objects and every `*_Summary` relationship without property types; the fixtures follow the spec, and the server reads either an object or an array.
- Money: the spec's money objects (`value`, `total_benefit_amount`, valuations) describe `example` and `description` as properties of `amount`, `formatted` and `currency`, so no real money value can pass the schema. This version therefore returns no money fields at all (amounts typed into free text are returned as written, see Safety defaults); valuations are the first thing to add once the live shape is known.
- Dates: task due dates follow the spec's example format `2021-01-01 00:00:00`; review dates are ISO 8601 with an offset. The server passes both through as given.
- The wording of Plannr's 404 and 422 messages for these endpoints (the mock uses the forms the spec documents for other endpoints) and of a 429 body (not documented; the mock uses Laravel's "Too Many Attempts.").
- Visibility: the spec says task lists are constrained by the user's role; the server returns whatever the token's login may see.
- How the API reacts to requests at the spaced rate here; the documented limit is 500 a minute per user.

## Going to production

This version runs locally over stdio with the user's own personal access token. For advisers to connect from claude.ai or ChatGPT without handling tokens, the next step is a remote server (Streamable HTTP) that uses the OAuth2 authorization code flow Plannr already offers, hosted by Plannr, so each user's own login and permissions apply, and then a listing in the Claude and ChatGPT connector directories. Valuations and other amounts can follow once the money shape is confirmed on a live account, and more write tools (completing tasks, updating review dates) once they can be tested on a test firm.

## Licence

MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.

TDQS

A3.9/5.0

Scored across 14 tools

Disambiguation4/5

Most tools are clearly distinct list/get pairs for clients, households, tasks, cases, and plans. There is mild overlap between search_clients and list_reviews_due (both surface clients from the same account endpoint), but descriptions describe distinct purposes (general search vs review-date filtering) adequately.

Naming Consistency4/5

Strong underlying list_*/get_* verb_noun pattern across clients, households, tasks, cases, and plans. Minor deviations: search_clients uses a different verb than the otherwise dominant list_* convention, and list_reviews_due/list_client_notes are more descriptive than schematic, but overall it is readable and predictable.

Tool Count5/5

14 tools is well within the sweet spot and each maps to a distinct resource or lookup. No redundant or trivial tools pad the set.

Completeness3/5

Read coverage is thorough (list+get for nearly every entity plus lookups like task statuses and logins), but there are no write/lifecycle operations such as update_task, create_case, or delete_plan. The mention that 'create_task needs one of these UUIDs' implies mutation tools exist elsewhere but are absent here, leaving a notable gap for a CRM-style server.

Maintenance

ActivityMaintained
ResponsivenessNo issues