Skip to main content
Glama
kenzotp

mcp-server-zuuna

README.md
# mcp-server-zuuna

[![CI](https://github.com/kenzotp/mcp-server-zuuna/actions/workflows/ci.yml/badge.svg)](https://github.com/kenzotp/mcp-server-zuuna/actions/workflows/ci.yml)

**Agent-native board access for [Zuuna](https://app.zuuna.de)** — a
[Model Context Protocol](https://modelcontextprotocol.io) server that lets coding agents
(Claude Code, Cursor, Codex) read and work a Zuuna board directly.

> Agents work the board. Git keeps it honest.
>
> The agent reads the board via this MCP server, does the work in the repo with its own
> coding tools, and when the PR merges, Zuuna's git integration moves the card. Zuuna
> provides the interface and the verifiable board truth — the agent writes the code, not Zuuna.

## Status

Working and tested, and **published on npm** (`npx -y mcp-server-zuuna`) after an auth
review of the token model, scopes and tool surface. Run from source (bottom) if you want
to hack on it.

## Tools

| Tool | What it does | Grounded in (Zuuna v1 API) | Scopes needed |
|---|---|---|---|
| `zuuna_me` | Token identity: org, plan, scopes, grace-window deadline | `GET /api/v1/me` | any token |
| `zuuna_boards` | List boards (non-archived, non-private) | `GET /api/v1/boards` | `boards:read` |
| `zuuna_board` | One board assembled: columns in order + cards (key, title, column, priority, type) — capped at one 200-card page; when more exist it returns `cardsTruncated: true` + `nextCursor` (pass as `cardsCursor`) | `GET /api/v1/boards/{id}/columns` + `GET /api/v1/boards/{id}/cards?limit=200` | `boards:read`, `cards:read` |
| `zuuna_card` | Full card detail by display key (`ZNA-2001`) or id | `GET /api/v1/cards/{idOrKey}` | `cards:read` |
| `zuuna_create_card` | Create a card (title required; column by id or title; board by id or key; optional `idempotencyKey` makes retries safe) | `POST /api/v1/boards/{id}/cards` | `cards:write` (+ `boards:read` for key/column resolution) |
| `zuuna_update_card` | Edit title / description / priority (`HIGHEST\|HIGH\|NORMAL\|LOW\|LOWEST`, null clears) | `PATCH /api/v1/cards/{idOrKey}` | `cards:write` |
| `zuuna_move_card` | Move a card to a column, by id or title | `PATCH /api/v1/cards/{idOrKey}` (after `cards:read` for title resolution) | `cards:read`, `cards:write` |
| `zuuna_comment` | Comment on a card (author = the token's creator) | `POST /api/v1/cards/{idOrKey}/comments` | `comments:write` |

Behavioral notes:

- Errors come back as MCP tool errors **with the API's own message** (the v1 envelope's
  `message` plus status and machine `code`) — e.g. `409 wip_limit_reached` when a HARD WIP
  limit refuses a move.
- One request, **no retries**: 4xx answers are never re-sent. Requests time out after
  15 s (configurable) and surface as network errors.
- Card display keys like `ZNA-2001` work anywhere a card is addressed — the v1 API accepts
  the key as the handle.
- `zuuna_board` pages the card list (200 cards per page, the v1 API's own cap) so a big
  board cannot flood the agent's context. When a page is not the whole board, the response
  says `cardsTruncated: true` and carries `nextCursor`; send it back as `cardsCursor` to
  fetch the next page.
- `zuuna_create_card` accepts an optional client-chosen `idempotencyKey`: re-sending the
  same key after a lost response or a 5xx returns the original card (200) instead of
  minting a duplicate.

## Setup

You need an API token from your Zuuna workspace. Give it the scopes for what the agent
should be allowed to do — a read-only observer needs only `boards:read` + `cards:read`.

The server is configured per client via environment variables:

| Variable | Default | Meaning |
|---|---|---|
| `ZUUNA_API_TOKEN` | — (required) | Bearer API token |
| `ZUUNA_BASE_URL` | `https://app.zuuna.de` | Zuuna base URL — must be an absolute http(s) URL (invalid values fail at startup); a plain-http value prints a cleartext-token warning to stderr |
| `ZUUNA_TIMEOUT_MS` | `15000` | Per-request timeout |

### Claude Code

```sh
claude mcp add zuuna \
  -e ZUUNA_API_TOKEN=zuuna_your_token \
  -- npx -y mcp-server-zuuna
```

or in the project's `.mcp.json`:

```json
{
  "mcpServers": {
    "zuuna": {
      "command": "npx",
      "args": ["-y", "mcp-server-zuuna"],
      "env": { "ZUUNA_API_TOKEN": "zuuna_your_token" }
    }
  }
}
```

### Cursor

In `~/.cursor/mcp.json` (or the project's `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "zuuna": {
      "command": "npx",
      "args": ["-y", "mcp-server-zuuna"],
      "env": { "ZUUNA_API_TOKEN": "zuuna_your_token" }
    }
  }
}
```

### Codex

In `~/.codex/config.toml`:

```toml
[mcp_servers.zuuna]
command = "npx"
args = ["-y", "mcp-server-zuuna"]
env = { "ZUUNA_API_TOKEN" = "zuuna_your_token" }
```

### Run from source

```sh
git clone https://github.com/kenzotp/mcp-server-zuuna
cd mcp-server-zuuna && npm install && npm run build
```

then point the client at the built binary instead of `npx`:

```sh
claude mcp add zuuna -e ZUUNA_API_TOKEN=zuuna_your_token -- node /path/to/mcp-server-zuuna/dist/index.js
```

## The loop

1. `zuuna_board` — the agent sees the board and picks a card (or you tell it: "do ZNA-2001").
2. The agent codes in your repo with its own tools, referencing the card key in commits.
3. The PR merges — and **git truth moves the card**: Zuuna's git integration (webhooks/CLI)
   advances the card through its columns. The agent never has to touch the board manually,
   and the board cannot drift from the repository.

## Honest scope

This server is deliberately **board I/O only**. There are no deploy tools and no git-write
tools — deploys and card movement from commits belong to Zuuna's git-truth engine, not to
the agent. Agents do the coding; Zuuna keeps the board honest about it.

## Development

```sh
npm install
npm run lint     # eslint
npm run build    # tsc -> dist/
npm test         # vitest, fully mocked (no network)
```

Optional live smoke test, strictly read-only (`GET /me` and `GET /boards` only — nothing
is ever created, moved or commented):

```sh
ZUUNA_E2E=1 ZUUNA_API_TOKEN=zuuna_your_token npm run test:e2e
```

Requires Node.js >= 20.

## License

[MIT](./LICENSE)

TDQS

A3.9/5.0

Scored across 8 tools

Disambiguation5/5

Each tool maps to a distinct resource+action: token identity, board listing, board detail, card detail, card create, card field update, card move, and card comment. The singular/plural pair (zuuna_board vs zuuna_boards) and explicit verb prefixes (create_, update_, move_) keep boundaries clear, and the only near-overlap (update vs move) is resolved by descriptions that separate field edits from column changes.

Naming Consistency4/5

All tools share the zuuna_ prefix and snake_case, with bare nouns for reads (zuuna_board, zuuna_card) and verb_noun for writes (zuuna_create_card, zuuna_update_card, zuuna_move_card) — a predictable read/write convention. Minor deviations: zuuna_me doesn't fit the noun/verb_noun pattern, and zuuna_comment omits the explicit action (zuuna_add_comment would match the other writers more closely).

Tool Count5/5

Eight tools is well-scoped for a board/card management server: one identity check, two board-level reads, one card read, and four card mutations. Every tool earns its place, with no redundancy or filler.

Completeness3/5

The core card workflow is covered (create, read, update title/description/priority, move, comment), but the surface is read-heavy: card detail exposes due dates, assignees, epics, and custom fields that no tool can write, and comments can be added but never retrieved. There is also no delete/archive operation for cards, which is an obvious gap in most card lifecycles.

Maintenance

ActivityMaintained
ResponsivenessNo issues