A minimal Jira-like task tracker exposed as an MCP server (stdio transport).
Tasks are stored as Markdown files with YAML frontmatter, and every write operation creates a Git commit.
- Node.js (tested with Node v22)
- Git CLI available in
PATH - A Git repository initialized at
~/.mcp_tracker/projects(the projects root), withuser.nameanduser.emailconfigured
No build step is required for the server. Install dependencies:
npm ciRun tests:
npm testStart the MCP server (stdio):
npm startor:
node server.jsThe CLI provides read-only access to project task lists without starting the MCP server.
Capabilities:
- Reads tasks from
~/.mcp_tracker/projects/<project> - Groups output by
backlog,todo, andin_progress - Does not perform Git writes or interact with active MCP sessions
- Shows a single task's metadata and body by project and ID
List tasks for a project:
npm run tasks:list -- --project <project>Output is grouped by backlog, todo, and in_progress, with lines in the form ID — Title.
Example:
npm run tasks:list -- --project ta-backendbacklog:
todo:
in_progress:
TB-065 — Example task title
Show task details (metadata + body):
npm run tasks:get -- --project <project> --id <ID>Output prints field: value pairs, followed by a blank line and the task body (if present).
Example:
npm run tasks:get -- --project ta-backend --id TB-065id: TB-065
project: ta-backend
type: user_story
title: Example task title
status: in_progress
created_at: 2026-01-21T10:00:00+00:00
started_at: 2026-01-21T10:05:00+00:00
tool: codex
Task body content...
The server stores projects under:
~/.mcp_tracker/projects
Important:
- The projects root (
~/.mcp_tracker/projects) must be a Git repository, because the server checks worktree state and creates commits on every write operation. - Example setup:
mkdir -p ~/.mcp_tracker/projects
cd ~/.mcp_tracker/projects
git init
git config user.email "you@example.com"
git config user.name "Your Name"
git commit --allow-empty -m "init"Each project is a directory named by:
^[a-z0-9-]+$
Each task is a single file:
~/.mcp_tracker/projects/<project>/<ID>.md
Naming rule:
- Task
ID(the filename and theidfrontmatter field) must match^[A-Z0-9-]+$(uppercase letters only).
Example:
---
id: FR-001
project: frontend
type: user_story
title: "My title"
status: backlog
created_at: 2026-01-21T15:03:23+05:00
---
## Description
Task body in Markdown...Notes:
titleis stored as a JSON string (quoted) in frontmatter.created_atuses ISO-8601 with UTC offset.
Add this server to Codex MCP servers:
codex mcp add mcp-tracker -- node /ABS/PATH/TO/mcp_tracker/server.jsList configured servers:
codex mcp listInspect a server config:
codex mcp get mcp-trackerRemove the server config:
codex mcp remove mcp-trackerAll tools return a JSON payload serialized as MCP text content.
- Success:
{ "ok": true, "data": ... }
- Error:
{ "ok": false, "error": { "code": string, "message": string } }
Lists valid projects (directories) under ~/.mcp_tracker/projects.
- Input:
{}(no parameters) - Output:
{ ok: true, data: { projects: string[] } }
Reads a task template file from the project directory based on the type input.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existtype(string): must match^[a-z0-9_-]+$(e.g.,story->STORY_TEMPLATE.md)
- File selection:
- The tool looks for
PROJECT_DIR/[TYPE]_TEMPLATE.mdwhere[TYPE]is the uppercasedtypevalue. - Example:
type: "bug"->BUG_TEMPLATE.md,type: "story"->STORY_TEMPLATE.md.
- The tool looks for
- Valid types are determined by the
*_TEMPLATE.mdfilenames present in the project directory. - Errors:
INVALID_TEMPLATE_TYPEwhentypedoes not match^[a-z0-9_-]+$.TASK_TEMPLATE_NOT_FOUNDwhen the expected*_TEMPLATE.mdfile is missing.
- Output:
{ ok: true, data: { template: string } }
Creates a task file in a project directory and commits the change.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existtype(enum):user_story|bugtitle(string): non-emptybody(string, optional)
- Output:
{ ok: true, data: { id, project, type, title, status, created_at } } - Notes:
idin output must match^[A-Z0-9-]+$
Updates an existing task and commits the change.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existid(string): task ID (also the filename without.md), must match^[A-Z0-9-]+$patch(object):type(optional enum):user_story|bugtitle(optional string): non-emptybody(optional string):""clears the body
- Rules:
- Only allowed when
status === "backlog"(otherwiseFORBIDDEN_UPDATE_IN_STATUS)
- Only allowed when
- Output:
{ ok: true, data: { id, project, type, title, status, created_at } }
Moves a task from backlog to todo and commits the change.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existid(string): task ID, must match^[A-Z0-9-]+$
- Rules:
- Only allowed when
status === "backlog"(otherwiseINVALID_STATUS_TRANSITION)
- Only allowed when
- Output:
{ ok: true, data: { id, project, type, title, status, created_at } }
Claims a task: moves it from todo to in_progress, sets started_at, optionally stores tool, and commits the change.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existid(string): task ID, must match^[A-Z0-9-]+$tool(string, optional): the claiming tool name
- Rules:
- Only allowed when
status === "todo"(otherwiseINVALID_STATUS_TRANSITION)
- Only allowed when
- Output:
{ ok: true, data: { id, project, type, title, status, created_at, started_at?, tool? } }
Completes a task: moves it from in_progress to done, sets done_at, and commits the change.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existid(string): task ID, must match^[A-Z0-9-]+$
- Rules:
- Only allowed when
status === "in_progress"(otherwiseINVALID_STATUS_TRANSITION)
- Only allowed when
- Output:
{ ok: true, data: { id, project, type, title, status, created_at, done_at? } }
Releases a task back to the queue: moves it from in_progress to todo, clears started_at and tool, and commits the change.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existid(string): task ID, must match^[A-Z0-9-]+$
- Rules:
- Only allowed when
status === "in_progress"(otherwiseINVALID_STATUS_TRANSITION)
- Only allowed when
- Output:
{ ok: true, data: { id, project, type, title, status, created_at } }
Cancels a task: moves it from backlog/todo/in_progress to canceled, sets canceled_at, and commits the change.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existid(string): task ID, must match^[A-Z0-9-]+$
- Rules:
- Not allowed when
statusisdoneorcanceled(otherwiseINVALID_STATUS_TRANSITION)
- Not allowed when
- Output:
{ ok: true, data: { id, project, type, title, status, created_at, canceled_at? } }
Lists tasks for a project with optional filtering.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existstatus(optional enum):backlog|todo|in_progress|done|canceledtype(optional enum):user_story|bugtext(optional string): case-insensitive substring match againsttitleandbody
- Output:
{ ok: true, data: { tasks: TaskView[] } } TaskView:id,project,type,title,status,created_at
Reads a single task by id and returns an extended representation (including body when it is non-empty).
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existid(string): task ID (also the filename without.md), must match^[A-Z0-9-]+$
- Output:
{ ok: true, data: TaskDetails } TaskDetails:id,project,type,title,status,created_atstarted_at?,done_at?,canceled_at?,tool?,body?
Time range report: counts done_count by done_at within [from, to] (inclusive) and remaining_count as the number of tasks whose status is not in {done, canceled}.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existfrom(string): ISO-8601 timestamp with UTC offsetto(string): ISO-8601 timestamp with UTC offset
- Output:
{ ok: true, data: { done_count: number, remaining_count: number } }
Returns Git history for the task file in a structured form.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existid(string): task ID, must match^[A-Z0-9-]+$
- Output:
{ ok: true, data: { commits: GitCommit[] } } GitCommit:hash,author,date,subject
Rolls back the task file to the specified Git revision and creates a separate commit like rollback <ID> to <rev>.
- Input:
project(string): must match^[a-z0-9-]+$and the directory must existid(string): task ID, must match^[A-Z0-9-]+$revision(string): git revision (hash/branch/tag)
- Output:
{ ok: true, data: TaskView }
Checks project and tasks integrity and returns a list of violations. Read-only: does not modify the repository and does not create Git commits.
- Input:
project(string): project name (can be invalid; in that case a violation is returned)
- Output:
{ ok: true, data: { violations: Violation[] } } Violation:code,message,details?
The server implements all tools listed in tools/list, including:
projects.listtasks.createtasks.gettasks.updatetasks.promote_to_todotasks.claimtasks.donetasks.releasetasks.canceltasks.listtasks.reporttasks.historytasks.rollbacktasks.verifytasks.template