Skip to content
Board Planner
GitHubOpen the app

Claude Code and MCP

The board speaks MCP, so an agent in your terminal can read the backlog, pick work up, and report back where the team can see it.

Tool Does
list_projects Every project the token can see
get_project One project by key or id, including its columns and fields
list_tasks Tasks, filtered by status, assignee, category or priority
get_task One task by key, for example ORB-14
create_task Create a task
update_task Change a task’s fields
change_task_status Move a task between columns
list_sprints Sprints in a project
create_sprint Create a sprint
update_sprint Change a sprint
add_comment Comment on a task
list_comments Read a task’s comments
link_tasks Link two tasks: a parent and its child, a blocker, a duplicate
unlink_tasks Remove one of those links

Project-defined fields go through a generic fields argument keyed by field name — {"Difficulty": "L"} — so a project that renames or removes a field does not break the client. Ask get_project which fields exist rather than assuming.

A tool refuses an argument it does not declare, rather than dropping it and answering as though the write had happened. The refusal names the argument and points at the replacement where there is one: a checklist passed to update_task is acceptanceCriteria, a status is the change_task_status tool, a difficulty is fields. An update that names no field at all is refused too, so a task’s updatedAt never moves for a call that wrote nothing.

link_tasks writes the relations the board already draws, so an epic can hold real sub-tasks instead of a checklist nothing can move, assign or run. Both ends are named by task key, and the type reads from taskKey’s side:

type Means
parent_of taskKey is the parent, targetTaskKey the child
blocked_by taskKey is blocked by targetTaskKey
relates The two are related, in no direction
duplicates taskKey duplicates targetTaskKey

The link is written on taskKey’s side. A second call replaces the relates, duplicates or parent_of link that end holds; blocked_by is kept separately and stacks with it. The far end keeps whatever it stored about taskKey, so linking a pair from both sides leaves the board holding both, and the task page shows two rows for the one pair.

parent_of is the exception, because a task has one parent: a second parent_of moves the child, and the child’s previous parent loses it without being named in the call.

Only relates reads the same from both ends. duplicates does not: the far task’s page shows Duplicated by and offers no way to remove it, so name the ends the way round you mean.

A link that would make a task its own ancestor, or close a chain of blockers, is refused. relates and duplicates carry no ordering, so there is no cycle for them to close.

Both tasks have to be on the same board — a pair on two boards is refused by name, rather than reading as a mistyped key.

get_task reads the links back from both ends: the parent lists its children, and the child names its parent without anything having been written to it.

unlink_tasks takes the same three arguments and removes the link stored on taskKey’s side, so it has to name the end that holds it. Asking from the other end is refused and says which end holds it, rather than reporting a removal that did not happen. Removing the same link twice is refused the second time for the same reason.

update_task carries an agent parameter, and setting it is the hand-over: the machine belonging to the task’s assignee takes the task and runs that agent, and only when that person assigned it to themselves. An empty string means nobody, which is what a task somebody is doing by hand looks like.

The agent is named, not identified — no response here carries an agent id. Names resolve within the task’s own project: a project agent belonging to another board is refused with exists on another project, rather than silently matching a board you did not mean when two of them share a name.

Who may set it follows the agent, not your instance role:

  • a project agent — anyone who can edit the task;
  • a global agent — the same;
  • a personal agent — only its owner, and only onto their own task. It is dropped again if the task is handed to somebody else.

An agent with no steps is refused, because a machine handed that task would have nothing to run.

The server is built into the app at POST /api/mcp. Nothing to clone, nothing to build — one URL and a bearer token:

{
"mcpServers": {
"boardplanner": {
"type": "http",
"url": "https://your-instance.example.com/api/mcp",
"headers": { "Authorization": "Bearer cp_..." }
}
}
}

Create the token under Settings → API Tokens, and scope it to the projects the agent should touch. The scope is enforced centrally, so the agent sees exactly what the token allows — no more than its owner, and often less.

Clients that expect a custom connector rather than a static token get a full OAuth 2.1 with PKCE, including dynamic client registration — so there is no client secret to paste. Point the client at the same /api/mcp URL, leave the client id and secret empty, sign in, and choose whether the connection gets all projects or only selected projects.

Tokens issued this way carry the same project scope as a scoped API token, on both REST and MCP.

Changing, setting or resetting your password disconnects Claude Code and revokes your API tokens — reconnect it afterwards. See A password change revokes more than sessions.

A standalone MCP server also ships in the repository for stdio clients. It exposes the twelve reading and writing tools above, but not link_tasks or unlink_tasks — new tools land on the HTTP endpoint only. It is configured with two environment variables — BOARDPLANNER_URL (defaulting to http://localhost:3000) and BOARDPLANNER_TOKEN. A bearer token is the only credential it accepts; there is no username-and-password mode.