Project tracker for AI agents: roadmap, periods, tasks and a validation gate, in one SQLite file
Runs on your machine from its package; the steps are below.
At most: creates or changes data: records, files, settings.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Run
uvx fronyboard
MCP client config
{
"mcpServers": {
"fronyboard": {
"command": "uvx",
"args": [
"fronyboard"
]
}
}
}An MCP server that gives AI agents (Claude Code and friends) a first-class project tracker.
Where Jira is an issue tracker for humans behind a web UI, FronyBoard replaces each part with something an agent can use natively:
| Jira | FronyBoard |
|---|---|
| Database | One SQLite file in a dedicated data directory |
| Records | JSON documents (roadmap, period) + Markdown |
| API | MCP tools |
| Workflow engine | Schema + rule validation, run as a gate before every write |
| State transition | An MCP tool call (transition_task) |
The schema and operating rules were extracted from a real product's management system (31 tasks shipped through it), then generalized.
Requires uv. One line registers FronyBoard in Claude Code;
uvx fetches the package from PyPI on first use and caches it:
claude mcp add FronyBoard -- uvx fronyboard
Any MCP client that can launch a stdio command works the same way — the command is
uvx fronyboard. It is also listed in the
MCP Registry as io.github.Cafelatte1/fronyboard.
From a clone, point at the checkout instead (this needs git):
git clone https://github.com/Cafelatte1/fronyboard
claude mcp add FronyBoard -- uv run --directory <path-to-clone>\backend fronyboard
This is the local (stdio) mode: the client starts the server as a child process and
talks to it over a pipe. No HTTP, no network, no credentials — web.py and
fauth.py are never called. Data is written to %LOCALAPPDATA%\Frony\FronyBoard\data
(~/.Frony/FronyBoard/data where LOCALAPPDATA is unset); set AIRA_DATA_DIR to
relocate it. Logs (JSON Lines, one line per MCP tool call plus server events) go to
the sibling logs folder — FRONYBOARD_LOG_DIR overrides.
To share one FronyBoard between several machines, or to use it from the Claude and ChatGPT apps, run it as an HTTP server instead — see docs/self-hosting.md.
Connecting the MCP server gives every session the tools and the general workflow
(delivered as server instructions). What it cannot know is which FronyBoard project
a codebase belongs to — declare that in the codebase itself by adding this section
to its CLAUDE.md (create the file if the project has none):
## FronyBoard
This project is tracked by FronyBoard (project key: DLY).
Manage tasks through the FronyBoard MCP tools, following the FronyBoard server instructions.
Replace DLY with the project's key (register one first with create_project).
The section is also the opt-in signal: a codebase without it is treated as not
FronyBoard-managed.
fronyboard.db
├── projects one row per project (key e.g. DLY): the roadmap record —
│ yearly overview (goal / now / target / checklist) + quarterly milestones
└── periods one row per opened period (e.g. 2026Q3): tasks ({KEY}-001, ...)
+ `result` (retrospective, written when the period closes)
A project is two kinds of records — the roadmap, and one record per period. Both are
JSON documents; the schema and the rules that guard it are in
backend/src/fronyboard/validation.py.
DLY-042) — they keep counting across
periods and are never reused. They are the only link between FronyBoard and a codebase:
use them in branch names (feat/DLY-042/short-desc) and record the branch on the task.depends_on, branch and two
one-line notes of at most 300 characters each. Agents read titles and status; the 25-line
body nobody read, and the month/week slots that only existed to schedule it, are gone.
Time is meta.completed_at.content is written at create time and says why the task exists — the pressure behind
it, or the scope deliberately left out. The approach chosen is not part of it (AIR-092):
agents wrote it here and again in the commit, so it lives in the commit only. check is required by
transition_task(status="done") and says what proves it done — the command and its
output, or an observable a reader can go and see. One free note asked before the work can
only restate the plan: in a 12-session benchmark every task an agent wrote paraphrased its
own title, because the note was fixed at create time and update_task was never called.
Reopening a task drops its check; nothing is proven any more.period → roadmap milestone (the quarter). That is the only one.planned | active | done;
tasks: todo | in_progress | done | blocked | cancelled.cancelled is the soft delete — there is no hard delete. Cancelling requires a
reason, keeps the record (and its id) forever, and hides the task from queries by
default (list_tasks takes include_cancelled). blocked = may resume,
cancelled = will not happen; transitioning a cancelled task restores it.get_status is the whole resume — the overview, each period's open tasks, and
recent_done: the ten most recently finished tasks with their content, their check
and when they completed. What is already built is what a cold session needs most, and
nobody makes a second call to find it (AIR-090). It is still a record of claims: the
board says what an agent reported, the code is the evidence.depends_on on a task lists the earlier tasks it builds on (other projects allowed).
It points backwards only — there is no forward index, because storing one direction and
deriving the other read as inverted often enough to be worth dropping (v0.36.0, AIR-089).
The field was follows until v0.37.0; the boot migration renames it. It is a pointer,
not a lock: list_tasks and get_status flag the entries not yet done as waiting_on,
and nothing is ever blocked.meta.created_at / updated_at / started_at / completed_at) are
stamped by the server in naive UTC — started_at on the first in_progress transition,
completed_at on done (and removed again if the task leaves done). Agents never
write them.result field closes a period — the rest of the file holds only current
state, so the "why it turned out this way" lives there: judgment and reasons,
not counts. Its presence is what marks a period closed.| Area | Tools |
|---|---|
| Projects | create_project, update_project, list_projects, get_roadmap, get_status, validate |
| Roadmap | set_overview, set_check, upsert_milestone |
| Periods | open_period, close_period, get_retrospective |
| Planning | create_task, update_task, transition_task |
| Queries | list_tasks, get_task, search_tasks, recent_activity |
update_task and transition_task derive the project from the task id prefix
(DLY-042 → DLY), so their key parameter is optional. Re-calling
close_period on a closed period rewrites its retrospective.
Typical flow:
create_project → set_overview → upsert_milestone → open_period
→ create_task
→ transition_task in_progress (with branch) → ... → transition_task done
→ close_period (retrospective)
Every mutation is validated before anything is written; invalid changes are rejected
with the full error list. close_period refuses while tasks are still todo or
in_progress. Writes are serialized per project, so concurrent clients cannot
collide on ids or lose updates.
The same package also runs as an always-on HTTP server (fronyboard serve): MCP over
streamable HTTP for every machine on your network, a read-only web dashboard for
humans, API keys per device, and OAuth for the hosted Claude / ChatGPT apps.
Authentication is delegated to FronyAuth,
a separate service. None of it is needed for the stdio install above.
To see the dashboard on your own machine without any of that, run fronyboard serve --local
(loopback only, no login) and open http://127.0.0.1:8642.
docs/self-hosting.md covers the setup;
docs/operations.md is the day-2 runbook.
uv run --directory backend pytest # backend
cd frontend; npm test # dashboard
The repo is a monorepo. backend/src/fronyboard/ — store.py (SQLite, data root),
validation.py (schema gate), service.py (operations), auth.py (bearer
middleware) + fauth.py (FronyAuth client), log.py, web.py (JSON API + static
serving), server.py (MCP tool surface + CLI). frontend/ — the dashboard (React +
Vite), built to static files that the backend serves; its build output
frontend/dist is committed so a server needs no Node toolchain.
The three docs under docs/ cover what the code cannot tell you — running this on your own machines: