zentra-mcp
# zentra-mcp
MCP server exposing Zentra's Task and Vault APIs as tools Claude can call
directly, from any project, not just from inside the Zentra repo:
| Tool | What it does |
|------|--------------|
| `zentra_task_create` | Create a task on the board, optionally filed straight into a sprint |
| `zentra_task_list` | List tasks, filtered by tag/status/type/parent |
| `zentra_task_move` | Move a task to a new status, optionally assigning it to a sprint |
| `zentra_sprint_list` | List sprints, optionally filtered by status (e.g. OPEN) |
| `zentra_vault_create_page` | Create a Vault page, optionally with content, a parent folder (by id or name), and a tag |
| `zentra_vault_list_pages` | List the Vault page tree, optionally narrowed to one page's children |
| `zentra_vault_upload_attachment` | Upload a local file as an attachment on a Vault page |
`zentra_sprint_list` resolves sprints (e.g. the OPEN one) by id, and that id can
then be passed as `sprintId` to `zentra_task_create` or `zentra_task_move` to
put a task in a sprint.
Extracted from the Zentra monorepo (`packages/mcp`) so that Zentra's sibling
projects can use it without checking out Zentra. It builds standalone: the few
task-board and Vault types it needs are vendored in `src/board-types.ts` rather
than imported from `@zentra/shared`.
## Setup (one time)
1. Install and build:
```bash
yarn install && yarn build
```
2. Authenticate. Two ways:
**Interactive** — run in a real terminal, not through Claude, since this is
the only place your password is typed:
```bash
node dist/cli.js login
```
This writes a JWT to `~/.config/zentra-mcp/credentials.json` (mode `600`).
The JWT expires after 7 days; re-run when a tool call reports "Session
expired."
**Unattended** — set `ZENTRA_TOKEN` to a long-lived service token, which
takes precedence over the credentials file. This is how the scheduled daily
runs authenticate; see `docs/service-token.md` in the Zentra repo for how to
mint one.
3. Register the server with Claude Code at **user scope**, so it's available in
every project:
```bash
claude mcp add zentra-mcp --scope user -- sh "$(pwd)/bin/zentra-mcp.sh"
```
`bin/zentra-mcp.sh` installs dependencies when `node_modules` is missing and
builds when `dist/` is missing **or stale** (any `src/*.ts` newer than
`dist/cli.js`), then execs the server. The missing case matters for fresh
checkouts, where MCP stdio servers are spawned before anything has had a
chance to build; the stale case matters after every `git pull`, since an
out-of-date `dist/` otherwise gets served silently and new tools never show
up. Build output goes to stderr so stdout stays a clean protocol channel.
Claude Code reads a server's tool list once, when it spawns the server at
session start. **Restart the session after changing tools** — a running
session will not pick them up.
## Configuration
- `ZENTRA_TOKEN` — bearer token to authenticate with, preferred over
`~/.config/zentra-mcp/credentials.json`. Used by unattended automation.
- `ZENTRA_API_URL` — override the API base URL (defaults to the production
Render deployment). Useful for developing against a local API.
## Project tags
Every task carries a `tag` naming its project, and a Vault page created via
`zentra_vault_create_page` can optionally carry one too.
Zentra's API is migrating tags from free text (`tags: string[]`) to a
registry (`GET /api/tags`, `tagIds: string[]`), but this server ships on its
own schedule, not in lockstep with that API — so it detects which one it's
talking to (by probing `GET /api/tags` once per session) and works either
way, with no configuration needed:
- **Registry available:** a `tag` name is resolved to its registered id
case-insensitively. Creating a task, or tagging a page, with a name that
isn't registered fails with a clear error; listing tasks by an unregistered
tag is treated as a legitimate "nothing matches" query and returns an empty
list instead. Register a project's tag in Zentra before using it here.
- **Registry not (yet) deployed:** tags are matched by exact string, after
trimming whitespace — any free text is accepted, there is no enforced set,
and asking for the wrong one returns an empty list rather than an error,
same as this server behaved before the registry existed.
This is transparent to callers either way — the same `tag` input works in
both worlds. See the comment above `resolveTagForWrite`/`resolveTagForFilter`
in `src/tag.ts` for the mechanics, and for the condition under which the
legacy path can eventually be deleted.
## Manual verification
There's no automated integration test against the live production API — that
would require storing real credentials in CI. After setup, verify by hand:
1. Start a Claude Code session in each project you use this from.
2. In each, ask Claude to create a task via `zentra_task_create` (it should
infer the `tag` from the project you're in).
3. Confirm each task appears correctly — right title, tag, status — on the
Zentra web UI's task board.
4. Ask Claude to list and move a task via `zentra_task_list`/`zentra_task_move`
and confirm the board reflects it.
5. Ask Claude to create a Vault page via `zentra_vault_create_page`, then attach
a local file to it via `zentra_vault_upload_attachment`. Confirm the page and
its attachment appear in the Vault on the web UI. Note that `filePath` is
read on the machine running this server, not on Claude's side.
6. Ask Claude to create a Vault page with `folder` set to the name of an
existing folder and `tag` set to a registered tag. Confirm the page lands
under that folder, tagged, on the web UI.
7. Ask Claude to list sprints via `zentra_sprint_list` with `status: "OPEN"`,
then create or move a task with that sprint's id as `sprintId`. Confirm the
task shows up in that sprint on the web UI.
TDQS
Scored across 3 tools
The three tools have clearly distinct purposes: create, list, and move (status update). There is no overlap in functionality, making it easy for an agent to select the right operation.
All tool names follow the identical pattern `zentra_task_<verb>` with snake_case. This consistent verb_noun structure makes the API predictable and easy to navigate.
At 3 tools, the server is concise but not under-powered for a focused task management use case. The count is within the well-scoped range, though slightly lean.
The set covers the core lifecycle: creation, listing/filtering, and status transitions. Missing operations like full detail retrieval or deletion are minor gaps that can often be worked around via listing, but a dedicated get/update would improve completeness.