saga-mcp 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,20 +5,21 @@
5
5
  [![license](https://img.shields.io/npm/l/saga-mcp)](https://github.com/spranab/saga-mcp/blob/master/LICENSE)
6
6
  [![IdeaCred](https://ideacred.com/api/badge/spranab/saga-mcp)](https://ideacred.com/profile/spranab)
7
7
 
8
- Your coding agent loses the plan between sessions. You come back tomorrow and
9
- it has no idea which of the five things you agreed on are done, which one is
10
- blocked on which, or why you rejected the second approach — because the plan
11
- lived in the context window, or in a `TODO.md` nobody updates.
8
+ Your coding agent loses the plan between sessions. You come back tomorrow and it has no idea which
9
+ of the five things you agreed on are done, which one is blocked on which, or why you rejected the
10
+ second approach — because the plan lived in the context window, or in a `TODO.md` nobody updates.
12
11
 
13
- saga-mcp gives the agent a real tracker instead: a SQLite file in your project
14
- holding projects, epics, tasks, subtasks, dependencies, comments, notes and
15
- decisions, exposed as 41 MCP tools. The agent writes to it as it works and
16
- reads the dashboard when it comes back. No accounts, no external service, no
17
- network calls — the database is a file you own.
12
+ saga-mcp gives the agent a real tracker instead: a SQLite file in your project holding projects,
13
+ epics, tasks, subtasks, dependencies, comments, notes and decisions, exposed as 41 MCP tools. The
14
+ agent writes to it as it works and reads it back when it returns. No accounts, no external service,
15
+ no network calls — the database is a file you own.
18
16
 
19
- ## Install (60 seconds)
17
+ ---
20
18
 
21
- Claude Code — add to your project's `.mcp.json`:
19
+ ## Install
20
+
21
+ Add saga-mcp to your MCP client. The same block works for Claude Code (`.mcp.json` in your
22
+ project), Claude Desktop (`claude_desktop_config.json`), and any other MCP client:
22
23
 
23
24
  ```json
24
25
  {
@@ -32,14 +33,30 @@ Claude Code — add to your project's `.mcp.json`:
32
33
  }
33
34
  ```
34
35
 
35
- Restart the client. `DB_PATH` is the only setting; the file and schema are
36
- created on first use.
36
+ Restart the client. `DB_PATH` is the only required setting; the file and its schema are created on
37
+ first use. Prefer a global install? `npm install -g saga-mcp`, then use `saga-mcp` as the command
38
+ instead of `npx`.
39
+
40
+ Tested on Node 20, 22 and 24, on Linux, macOS and Windows.
41
+
42
+ ### Settings
43
+
44
+ | Variable | Required | Description |
45
+ |----------|----------|-------------|
46
+ | `DB_PATH` | Yes | Path to the `.tracker.db` SQLite file. Created on first use. |
47
+ | `SAGA_PROJECT` | No | Scope every tool to one project, by id or name. Set this per repo when several repos [share one database](#one-database-many-projects). |
48
+ | `SAGA_DESCRIPTION_LOCK` | No | `on_progress` locks a task's description the moment work starts on it. Off by default. See [keeping agents on the rails](#keeping-agents-on-the-rails). |
49
+ | `SAGA_TOOLS` | No | `full` (default) lists all 41 tools. `core` lists only the 13 an ordinary tracking session needs, saving ~4,300 tokens per session. See [token cost](#token-cost). |
50
+
51
+ No API keys, no accounts, no external services.
37
52
 
38
- ## What it looks like
53
+ ---
54
+
55
+ ## Your first session
39
56
 
40
57
  **You:** "Set up tracking for the e-commerce API and plan out auth."
41
58
 
42
- ```
59
+ ```js
43
60
  tracker_init({ project_name: "E-Commerce API" })
44
61
  epic_create({ project_id: 1, name: "Authentication", priority: "high" })
45
62
  task_create({ epic_id: 1, title: "Design auth schema", priority: "critical" })
@@ -47,306 +64,308 @@ task_create({ epic_id: 1, title: "Implement JWT auth", depends_on: [1] })
47
64
  task_create({ epic_id: 1, title: "Add OAuth2 Google login", depends_on: [2] })
48
65
  ```
49
66
 
50
- Tasks 2 and 3 come back **blocked** — their dependencies aren't done. Finish
51
- task 1 and task 2 unblocks itself.
67
+ Tasks 2 and 3 come back **blocked** — their dependencies aren't done. Finish task 1 and task 2
68
+ unblocks itself.
52
69
 
53
70
  **Next session, you:** "Where were we?"
54
71
 
72
+ ```js
73
+ tracker_next({})
74
+ -> Work on #1 'Design auth schema' — critical priority, in the active epic
75
+ 'Authentication'. 2 other task(s) are blocked.
55
76
  ```
56
- tracker_dashboard({})
57
- → "E-Commerce API: 5 tasks across 2 epics. 40% complete.
58
- Active: Authentication (2/3 done). Next up: Product Catalog (2 tasks).
59
- 1 blocked task(s)."
60
- ```
61
-
62
- Plus the structured data behind it: stats, epics, blocked and overdue tasks,
63
- recent activity, notes.
64
77
 
65
- ## Features
78
+ One recommendation with the reason. For the whole picture instead, `tracker_dashboard({})` returns
79
+ stats, epics, blocked and overdue tasks, recent activity and notes, with a summary on top.
66
80
 
67
- - **Full hierarchy**: Projects > Epics > Tasks > Subtasks
68
- - **Task dependencies**: Express sequencing with auto-block/unblock when deps are met
69
- - **Description lock**: Stop agents rewriting a task's spec when they meant to leave a comment
70
- - **Subtask ordering & dependencies**: Explicit order, and checklist items that wait on siblings
71
- - **Comments**: Threaded discussions on tasks — leave breadcrumbs across sessions, with reversible soft-delete
72
- - **Web UI**: `saga-web` serves a local dashboard for browsing *and* editing the same database
73
- - **Templates**: Reusable task sets with `{variable}` substitution
74
- - **Dashboard**: One tool call gives full overview with natural language summary
75
- - **SQLite**: Self-contained `.tracker.db` file per project — zero setup, no external database
76
- - **Activity log**: Every mutation is automatically tracked with old/new values
77
- - **Notes system**: Decisions, context, meeting notes, blockers — all searchable
78
- - **Batch operations**: Create multiple subtasks or update multiple tasks in one call
79
- - **41 focused tools**: With MCP safety annotations on every tool
80
- - **Import/export**: Full project backup and migration as JSON (with dependencies and comments)
81
- - **Source references**: Link tasks to specific code locations
82
- - **Auto time tracking**: Hours computed automatically from activity log
83
- - **Cross-platform**: Works on macOS, Windows, and Linux
81
+ And when you would rather look than ask, `saga-web` puts the same database in a browser:
84
82
 
85
- ## Other clients
83
+ <picture>
84
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/overview-dark.png">
85
+ <img alt="The saga-web overview: task counts, per-epic progress, blocked and overdue work" src="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/overview-light.png">
86
+ </picture>
86
87
 
87
- ### Claude Code
88
88
 
89
- Add to your project's `.mcp.json`:
89
+ ---
90
90
 
91
- ```json
92
- {
93
- "mcpServers": {
94
- "saga": {
95
- "command": "npx",
96
- "args": ["-y", "saga-mcp"],
97
- "env": {
98
- "DB_PATH": "/absolute/path/to/your/project/.tracker.db"
99
- }
100
- }
101
- }
102
- }
103
- ```
91
+ ## What you get
104
92
 
105
- ### With Claude Desktop
93
+ - **[The next thing to do](#asking-what-to-do-next)** — one recommendation with its reason, at a
94
+ third the cost of the dashboard
95
+ - **[Real sequencing](#ordering-and-dependencies)** — dependencies that auto-block and auto-unblock,
96
+ a manual order the tools respect, and cycles refused rather than deadlocked
97
+ - **[Guards against agent drift](#keeping-agents-on-the-rails)** — a lockable description, and
98
+ prerequisites enforced on write rather than merely reported
99
+ - **[A web UI](#web-ui)** — `saga-web` serves the same database in a browser, read *and* write
100
+ - **[Templates](#templates)** — reusable task sets with `{variable}` substitution, editable in place
101
+ - **[Archiving and soft delete](#getting-old-work-out-of-the-way)** — get finished work out of the
102
+ context you pay for, reversibly
103
+ - **[Forgiving input](#forgiving-input)** — a smaller model sending an array as a JSON string
104
+ doesn't silently collapse your batch into one record
105
+ - **[One file, many projects](#one-database-many-projects)** — per-repo databases or one shared file
106
+ - **A full audit trail** — every mutation logged with old and new values, and nothing an agent
107
+ removes is unrecoverable
108
+ - **41 tools** with MCP safety annotations on every one, and a [tiered surface](#token-cost) when
109
+ you want a smaller context bill
106
110
 
107
- Add to your Claude Desktop config (`claude_desktop_config.json`):
111
+ ---
108
112
 
109
- ```json
110
- {
111
- "mcpServers": {
112
- "saga": {
113
- "command": "npx",
114
- "args": ["-y", "saga-mcp"],
115
- "env": {
116
- "DB_PATH": "/absolute/path/to/your/project/.tracker.db"
117
- }
118
- }
119
- }
120
- }
121
- ```
113
+ ## Asking what to do next
122
114
 
123
- ### Manual install
115
+ `tracker_dashboard` hands an agent everything and leaves it to reason. `tracker_next` answers the
116
+ question:
124
117
 
125
- ```bash
126
- npm install -g saga-mcp
127
- DB_PATH=./my-project/.tracker.db saga-mcp
118
+ ```
119
+ tracker_next()
120
+ -> Work on #12 'Write the adapter' — already in progress, high priority, in the
121
+ active epic 'Provider swap'. Next step: implement. Also overdue: #18 'Renew cert'.
122
+ 3 other task(s) are blocked.
128
123
  ```
129
124
 
130
- ## Configuration
131
-
132
- saga-mcp requires a single environment variable:
125
+ One recommendation with the reason, the next unfinished subtask inside it, a couple of
126
+ alternatives, and anything overdue or blocked. About a third the size of the dashboard.
133
127
 
134
- | Variable | Required | Description |
135
- |----------|----------|-------------|
136
- | `DB_PATH` | Yes | Absolute path to the `.tracker.db` SQLite file. The file and schema are auto-created on first use. |
137
- | `SAGA_PROJECT` | No | Scope every tool to one project, by id or name. Set this per repo when several repos share one database. Unset, tools read across the whole file. |
138
- | `SAGA_TOOLS` | No | `full` (default) lists all 41 tools. `core` lists only the 13 an ordinary tracking session needs, cutting ~3,300 tokens of context per session. Tools left off the list still work if called by name. |
128
+ The ordering rule worth knowing: **continuing beats starting.** A task already in progress outranks
129
+ an untouched one that is overdue or higher priority, because abandoning work in flight just leaves
130
+ two things unfinished — the overdue work is named in the summary instead. Blocked tasks are never
131
+ recommended, archived epics and removed tasks are skipped, and subtask dependencies decide which
132
+ step comes next inside the chosen task.
139
133
 
140
- No API keys, no accounts, no external services. Everything is stored locally in the SQLite file you specify.
134
+ When nothing is actionable it says what to unblock rather than returning an empty answer:
141
135
 
142
- ### Token cost
136
+ ```
137
+ Nothing is actionable: all 4 remaining task(s) are blocked.
138
+ Unblocking #7 'the keystone' would release 3 of them.
139
+ ```
143
140
 
144
- The tool list is context every session pays before any work happens, and list responses are
145
- context it pays again on every call. Both are kept deliberately small:
141
+ ---
146
142
 
147
- - Responses are compact JSON — no pretty-print indentation, which measured 20-27% of every response
148
- - `task_list` rows omit nulls and `metadata`, and truncate descriptions to 120 characters
149
- (call `task_get` for a task's full text) — 19-39% smaller depending on how long your descriptions run
150
- - `activity_log` omits null columns and the row id (no tool takes one) — about 27% smaller
151
- - `tracker_search` returns previews rather than whole records — about 47% smaller; follow up with
152
- `task_get` or `note_list` for the full text
153
- - `SAGA_TOOLS=core` drops the listed tool surface from ~6,000 to ~2,700 tokens
143
+ ## Ordering and dependencies
154
144
 
155
- `note_list` deliberately keeps full note content — it is the retrieval tool, not a preview.
145
+ **A deliberate order wins over a guess.** `task_list` sorts by priority until someone arranges an
146
+ epic, and from then on it follows the arrangement:
156
147
 
157
- Set `SAGA_TOOLS=core` when an agent only tracks work; leave it unset when you want templates,
158
- import/export, session diffs and the rest discoverable.
148
+ ```js
149
+ task_reorder({ epic_id: 2, ordered_ids: [8, 5, 6] })
150
+ task_list({ epic_id: 2 }) // 8, 5, 6 — the plan, in order
151
+ task_list({ epic_id: 2, sort_by: "priority" }) // priority, if that is what you want
152
+ ```
159
153
 
160
- ## Tools
154
+ Priority is a reasonable guess about what matters; a sequence someone wrote down is not a guess.
155
+ An agent handed a plan should start at the beginning of it, not at whichever step happens to be
156
+ marked critical.
161
157
 
162
- ### Getting Started
158
+ Nothing changes for epics nobody has arranged — those sort by priority exactly as before, and an
159
+ explicit `sort_by` is always obeyed literally.
163
160
 
164
- | Tool | Description | Annotations |
165
- |------|-------------|-------------|
166
- | `tracker_init` | Initialize tracker and create first project | `readOnly: false`, `idempotent: true` |
167
- | `tracker_next` | What to work on next, with the reason and what is blocked | `readOnly: true` |
168
- | `tracker_dashboard` | Full project overview with natural language summary | `readOnly: true` |
161
+ Anything omitted from `ordered_ids` keeps its relative position at the end. `sort_order` runs
162
+ ascending — lower sorts first — and a task created *after* an arrangement has no place in it, so it
163
+ lands at the end rather than the front. In the web UI you can drag tasks into place inside an epic.
169
164
 
170
- ### Projects
165
+ Task dependencies auto-block and auto-unblock:
171
166
 
172
- | Tool | Description | Annotations |
173
- |------|-------------|-------------|
174
- | `project_create` | Create a new project | `readOnly: false` |
175
- | `project_list` | List projects with completion stats | `readOnly: true` |
176
- | `project_update` | Update project (archive to soft-delete) | `readOnly: false`, `idempotent: true` |
167
+ ```js
168
+ task_update({ id: 9, depends_on: [8] }) // 9 becomes blocked while 8 is open
169
+ ```
177
170
 
178
- ### Epics
171
+ Re-evaluation runs whenever a blocker's *doneness* changes in either direction, so reopening a
172
+ finished blocker blocks its dependents again, and clearing the last dependency releases them.
173
+ Circular dependencies are refused with the loop named, for tasks and subtasks alike — anything
174
+ in a cycle would be blocked forever. The web UI shows a banner at the top of a blocked task naming
175
+ what it waits on, with a picker to add or remove dependencies.
179
176
 
180
- | Tool | Description | Annotations |
181
- |------|-------------|-------------|
182
- | `epic_create` | Create an epic within a project | `readOnly: false` |
183
- | `epic_list` | List epics with task counts | `readOnly: true` |
184
- | `epic_archive` | Archive/unarchive an epic, hiding it and its tasks from listings | `readOnly: false`, `idempotent: true` |
185
- | `epic_update` | Update an epic | `readOnly: false`, `idempotent: true` |
177
+ ---
186
178
 
187
- ### Tasks
179
+ ## Keeping agents on the rails
188
180
 
189
- | Tool | Description | Annotations |
190
- |------|-------------|-------------|
191
- | `task_create` | Create a task with optional dependencies | `readOnly: false` |
192
- | `task_list` | List/filter tasks with dependency info | `readOnly: true` |
193
- | `task_get` | Get task with subtasks, notes, comments, and dependencies | `readOnly: true` |
194
- | `task_update` | Update task (auto-logs, auto-blocks/unblocks) | `readOnly: false`, `idempotent: true` |
195
- | `task_lock_description` | Lock/unlock a description so agents can't rewrite it | `readOnly: false`, `idempotent: true` |
196
- | `task_reorder` | Set the order of an epic's tasks | `readOnly: false`, `idempotent: true` |
197
- | `task_delete` | Remove a `todo` task (soft delete, restorable) | `readOnly: false`, `idempotent: true` |
198
- | `task_restore` | Restore a removed task | `readOnly: false`, `idempotent: true` |
199
- | `task_batch_update` | Update multiple tasks at once | `readOnly: false`, `idempotent: true` |
181
+ Two guards for the ways an agent goes wrong on a long task.
200
182
 
201
- ### Subtasks
183
+ **A locked description.** Agents sometimes rewrite a task's description to record progress, when
184
+ they meant to add a comment — and the spec you agreed on is gone. Lock it and `task_update` refuses:
202
185
 
203
- | Tool | Description | Annotations |
204
- |------|-------------|-------------|
205
- | `subtask_create` | Create subtask(s) — supports batch | `readOnly: false` |
206
- | `subtask_update` | Update title/status/position; `depends_on` and `blocks` set ordering | `readOnly: false`, `idempotent: true` |
207
- | `subtask_reorder` | Set the order of a task's subtasks in one call | `readOnly: false`, `idempotent: true` |
208
- | `subtask_delete` | Delete subtask(s) — supports batch | `destructive: true`, `idempotent: true` |
186
+ ```js
187
+ task_lock_description({ id: 12 })
188
+ task_update({ id: 12, description: "..." })
189
+ -> Task 12's description is locked and was not changed. Record progress with
190
+ comment_add instead, or unlock it in the web UI if the description is genuinely wrong.
191
+ ```
209
192
 
210
- ### Comments
193
+ Everything else about the task stays editable — the point is to protect the spec, not freeze the
194
+ task. The lock cannot be cleared as a side effect of an ordinary `task_update`; it takes a
195
+ deliberate `task_lock_description` call or the lock toggle in the web UI, and both are logged.
211
196
 
212
- | Tool | Description | Annotations |
213
- |------|-------------|-------------|
214
- | `comment_add` | Add a comment to a task (threaded discussion) | `readOnly: false` |
215
- | `comment_list` | List comments on a task (removed ones hidden unless `include_deleted`) | `readOnly: true` |
216
- | `comment_delete` | Remove a comment — soft delete, row kept for audit | `readOnly: false`, `idempotent: true` |
217
- | `comment_restore` | Restore a removed comment | `readOnly: false`, `idempotent: true` |
197
+ This is a guard against confusion, not an adversarial control: an agent that is told to unlock
198
+ still can. It turns a silent overwrite into a visible, reversible decision.
218
199
 
219
- ### Templates
200
+ Locking each task by hand does not scale across a plan, so `SAGA_DESCRIPTION_LOCK=on_progress`
201
+ does it for you: a task locks as work starts on it — when it moves to `in_progress`, `review` or
202
+ `done`, or is created there. It is off unless you set it, since it changes what an agent is
203
+ allowed to do.
220
204
 
221
- | Tool | Description | Annotations |
222
- |------|-------------|-------------|
223
- | `template_create` | Create a reusable task template with `{variable}` placeholders | `readOnly: false` |
224
- | `template_list` | List templates; `include_tasks` shows what each one creates | `readOnly: true` |
225
- | `template_update` | Edit a template in place — name, description or tasks | `readOnly: false`, `idempotent: true` |
226
- | `template_apply` | Apply template to create tasks with variable substitution | `readOnly: false` |
227
- | `template_delete` | Delete a template | `destructive: true`, `idempotent: true` |
205
+ ```json
206
+ "env": { "DB_PATH": "…", "SAGA_DESCRIPTION_LOCK": "on_progress" }
207
+ ```
228
208
 
229
- ### Notes
209
+ It sets the same flag `task_lock_description` sets, rather than second-guessing every write. So
210
+ one mechanism governs the field: unlock a task and it stays unlocked, whatever its status does
211
+ next. `blocked` is not "started" — a task waiting on a dependency has not been worked on, and
212
+ dependencies move tasks in and out of that status on their own.
230
213
 
231
- | Tool | Description | Annotations |
232
- |------|-------------|-------------|
233
- | `note_save` | Create or update a note (upsert) | `readOnly: false` |
234
- | `note_list` | List notes with filters | `readOnly: true` |
235
- | `note_search` | Full-text search across notes | `readOnly: true` |
236
- | `note_delete` | Delete a note | `destructive: true`, `idempotent: true` |
214
+ **Subtask order and dependencies.** New subtasks are appended in order rather than all landing at
215
+ position 0, `subtask_reorder` sets the order in one call (or drag them in the UI), and a subtask
216
+ can wait on its siblings:
237
217
 
238
- ### Intelligence
218
+ ```js
219
+ subtask_update({ id: 8, depends_on: [5, 6] }) // 8 waits for 5 and 6
220
+ subtask_update({ id: 4, blocks: [5, 6, 7, 8] }) // a bug that holds up the rest
221
+ ```
239
222
 
240
- | Tool | Description | Annotations |
241
- |------|-------------|-------------|
242
- | `tracker_search` | Cross-entity search (projects, epics, tasks, notes) | `readOnly: true` |
243
- | `activity_log` | View change history with filters | `readOnly: true` |
244
- | `tracker_session_diff` | Show what changed since a given timestamp — call at session start | `readOnly: true` |
223
+ Reads carry `depends_on` and `blocked`, and the block is **enforced on write**: starting or
224
+ finishing a subtask whose prerequisites are unmet is refused, and so is completing a task whose
225
+ checklist is still open.
245
226
 
246
- ### Import / Export
227
+ ```js
228
+ subtask_update({ id: 8, status: "in_progress" })
229
+ -> Subtask 8 cannot be started — it waits on #5 'write the parser' (todo).
230
+ Finish those first, or pass force: true to override deliberately (the override is logged).
231
+ ```
247
232
 
248
- | Tool | Description | Annotations |
249
- |------|-------------|-------------|
250
- | `tracker_export` | Export full project as nested JSON (includes dependencies and comments) | `readOnly: true` |
251
- | `tracker_import` | Import project from JSON (matching export format) | `readOnly: false` |
233
+ `force: true` is the way past, for when a person has decided the blocker no longer applies. It
234
+ works on `subtask_update`, `task_update` and `task_batch_update`, and every override is written to
235
+ the activity log naming what was skipped. The web UI asks for confirmation and then sends it.
252
236
 
253
- ## Usage Examples
237
+ The distinction that matters is between an agent quietly ignoring a blocker and someone choosing to
238
+ override one. Dependencies stay within one task — a checklist item waiting on something under a
239
+ *different* task is a task-level dependency, and `task_update depends_on` already models that.
254
240
 
255
- ### Example 1: Starting a project with dependencies
241
+ ---
256
242
 
257
- **User prompt:** "Set up tracking for my new e-commerce API project"
243
+ ## Comments as a decision trail
258
244
 
259
- **Tool calls:**
260
- ```
261
- tracker_init({ project_name: "E-Commerce API", project_description: "REST API for online store" })
262
- epic_create({ project_id: 1, name: "Authentication", priority: "high" })
263
- task_create({ epic_id: 1, title: "Design auth schema", priority: "critical" })
264
- task_create({ epic_id: 1, title: "Implement JWT auth", priority: "high", depends_on: [1] })
265
- task_create({ epic_id: 1, title: "Add OAuth2 Google login", priority: "medium", depends_on: [2] })
245
+ ```js
246
+ comment_add({ task_id: 5, content: "Investigated root cause: CORS headers missing on preflight" })
247
+ comment_add({ task_id: 5, content: "Fixed by adding OPTIONS handler. Tested with curl." })
248
+ task_update({ id: 5, status: "done" })
266
249
  ```
267
250
 
268
- **Result:** Task 2 and 3 are auto-blocked because their dependencies aren't done yet. When task 1 is marked done, task 2 auto-unblocks.
251
+ Comments persist across sessions — next time an agent calls `task_get(5)`, it sees the full thread.
269
252
 
270
- ### Example 2: Resuming work with dashboard summary
253
+ If a comment turns out to be wrong, retract it without losing the trail:
271
254
 
272
- **Tool calls:**
273
- ```
274
- tracker_dashboard({})
255
+ ```js
256
+ comment_delete({ id: 12, reason: "Root cause was wrong — it was a proxy timeout", deleted_by: "pranab" })
275
257
  ```
276
258
 
277
- **Response includes a natural language summary:**
278
- ```
279
- "E-Commerce API: 5 tasks across 2 epics. 40% complete. Active: Authentication (2/3 done). Next up: Product Catalog (2 tasks). 1 blocked task(s)."
280
- ```
259
+ The row stays in the database and in the activity log. `comment_list` and `task_get` skip it,
260
+ `comment_list({ task_id: 5, include_deleted: true })` shows it with its reason, and
261
+ `comment_restore({ id: 12 })` brings it back. Nothing an agent removes is unrecoverable.
281
262
 
282
- Plus the full structured data (stats, epics, blocked tasks, overdue tasks, activity, notes).
263
+ ---
283
264
 
284
- ### Example 3: Using templates for repeated workflows
265
+ ## Templates
285
266
 
286
- **Create a template:**
287
- ```
267
+ A reusable set of tasks with `{variable}` placeholders, filled in when applied:
268
+
269
+ ```js
288
270
  template_create({
289
271
  name: "feature_workflow",
290
- description: "Standard feature implementation",
291
272
  tasks: [
292
- { "title": "Design {feature} API", "priority": "critical", "estimated_hours": 2 },
293
- { "title": "Implement {feature}", "priority": "high", "estimated_hours": 8 },
294
- { "title": "Write tests for {feature}", "priority": "high", "estimated_hours": 4 },
295
- { "title": "Document {feature}", "priority": "medium", "estimated_hours": 1 }
273
+ { title: "Design {feature} API", priority: "critical", estimated_hours: 2 },
274
+ { title: "Implement {feature}", priority: "high", estimated_hours: 8 },
275
+ { title: "Write tests for {feature}", priority: "high", estimated_hours: 4 }
296
276
  ]
297
277
  })
298
- ```
299
278
 
300
- **Apply it:**
301
- ```
302
- template_apply({ template_id: 1, epic_id: 2, variables: { "feature": "user auth" } })
279
+ template_apply({ template_id: 1, epic_id: 2, variables: { feature: "user auth" } })
280
+ // -> "Design user auth API", "Implement user auth", "Write tests for user auth"
303
281
  ```
304
282
 
305
- Templates are editable in place, which matters because the id is what
306
- `template_apply` refers to:
283
+ Templates are **editable in place**, which matters because the id is what `template_apply` refers
284
+ to — recreating one breaks anything holding it:
307
285
 
308
286
  ```js
309
- // change just the name; the tasks are untouched
310
- template_update({ id: 1, name: "Feature rollout" })
287
+ template_update({ id: 1, name: "Feature rollout" }) // tasks untouched
288
+ template_update({ id: 1, tasks: [{ title: "Design {feature}" }] }) // name untouched
289
+ template_list({ include_tasks: true }) // see what one creates
290
+ ```
291
+
292
+ Task definitions are checked when written rather than when applied, so a bad priority or a missing
293
+ title is refused up front instead of failing later against an epic you have already chosen.
294
+ Templates live in the database as a whole, not inside one project.
295
+
296
+ The Templates tab shows what each one creates, and the `{placeholders}` it will ask for:
297
+
298
+ <picture>
299
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/templates-dark.png">
300
+ <img alt="The templates tab, showing each template with the tasks it creates" src="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/templates-light.png">
301
+ </picture>
311
302
 
312
- // or replace the task list wholesale
313
- template_update({ id: 1, tasks: [{ title: "Design {feature}", priority: "high" }] })
314
303
 
315
- // see what a template actually creates, rather than just how many tasks
316
- template_list({ include_tasks: true })
304
+ ---
305
+
306
+ ## Getting old work out of the way
307
+
308
+ An epic list that is mostly finished work, and tasks an agent created that should have been
309
+ subtasks, are context you pay for on every call.
310
+
311
+ ```js
312
+ epic_archive({ id: 4 }) // the epic and its tasks drop out of listings
313
+ task_delete({ id: 12, reason: "should have been a subtask" })
317
314
  ```
318
315
 
319
- Creates 4 tasks: "Design user auth API", "Implement user auth", "Write tests for user auth", "Document user auth".
316
+ Archiving is deliberately **not** the `cancelled` status: `cancelled` means "we decided not to do
317
+ this", while most of what you want to archive is *completed*. Archived epics and their tasks
318
+ disappear from `epic_list`, `tracker_dashboard`, `task_list` and `tracker_search` — including the
319
+ statistics, not just the lists — and come back with `include_archived`. In the web UI both the
320
+ Overview and the Epics tab archive an epic, hide archived ones by default, and share one
321
+ show-archived switch.
320
322
 
321
- ### Example 4: Task comments as decision trail
323
+ Nothing vanishes silently. The dashboard says what it left out:
322
324
 
323
325
  ```
324
- comment_add({ task_id: 5, content: "Investigated root cause: CORS headers missing on preflight" })
325
- comment_add({ task_id: 5, content: "Fixed by adding OPTIONS handler. Tested with curl." })
326
- task_update({ id: 5, status: "done" })
326
+ Hidden: 2 archived epic(s) and 1 removed task(s) — pass include_archived to include them.
327
327
  ```
328
328
 
329
- Comments persist across sessions — next time an agent calls `task_get(5)`, it sees the full discussion thread.
329
+ `task_delete` is the same soft delete comments have, restricted to tasks still in `todo`: anything
330
+ further along has comments, time tracking and an activity log that removing it would strand, and a
331
+ task other tasks depend on is refused outright so nothing is left blocked forever. The row is kept,
332
+ `task_restore` brings it back, and `tracker_export` includes archived and removed rows because a
333
+ backup that omits things is not a backup.
330
334
 
331
- If a comment turns out to be wrong, retract it without losing the trail:
335
+ ---
332
336
 
333
- ```
334
- comment_delete({ id: 12, reason: "Root cause was wrong — it was a proxy timeout", deleted_by: "pranab" })
337
+ ## Forgiving input
338
+
339
+ Smaller models routinely send an array parameter as a *string* containing JSON. Every array-taking
340
+ tool accepts that, so a batch does not silently collapse into one record:
341
+
342
+ ```js
343
+ subtask_create({ task_id: 3, titles: '["Write it","Test it"]' }) // 2 subtasks
344
+ subtask_create({ task_id: 3, titles: "- Write it\n- Test it" }) // 2 subtasks
345
+ task_batch_update({ ids: "[4,5]", status: "done" }) // both tasks
346
+ task_create({ epic_id: 1, title: "x", tags: "billing, urgent" }) // 2 tags
335
347
  ```
336
348
 
337
- The row stays in the database and in the activity log. `comment_list` and `task_get` skip it,
338
- `comment_list({ task_id: 5, include_deleted: true })` shows it with its reason, and
339
- `comment_restore({ id: 12 })` brings it back. Nothing an agent removes is unrecoverable.
349
+ Coercion stops where intent becomes ambiguous. A comma inside a *title* is left alone —
350
+ `"Design the API, then implement it"` is one subtask, not two — while a comma in a tag or an id
351
+ list is a separator, because neither can contain one. Anything genuinely unusable is refused with a
352
+ message naming what arrived and what was wanted, rather than a leaked `ids.map is not a function`.
353
+
354
+ What comes back matches what went in. `tags`, `metadata` and `source_ref` live in JSON text
355
+ columns, and every response decodes them at the boundary, so a tag list reads as `["billing",
356
+ "urgent"]` rather than the escaped string `"[\"billing\",\"urgent\"]"` an agent cannot use.
357
+
358
+ ---
340
359
 
341
360
  ## One database, many projects
342
361
 
343
- saga-mcp works either way: a `.tracker.db` per repo (portable, keeps unrelated work apart),
344
- or one shared database that every repo points at.
362
+ saga-mcp works either way: a `.tracker.db` per repo (portable, keeps unrelated work apart), or one
363
+ shared database that every repo points at.
345
364
 
346
365
  The shared setup needs one extra thing. `projects` is the top-level table, so a shared file holds
347
- several projects — but `task_list`, `note_list`, `activity_log` and `tracker_search` read across
348
- the whole file unless told otherwise. An agent in repo B would see repo A's tasks. Set
349
- `SAGA_PROJECT` per repo and each agent sees only its own:
366
+ several projects — but `task_list`, `note_list`, `activity_log` and `tracker_search` read across the
367
+ whole file unless told otherwise. An agent in repo B would see repo A's tasks. Set `SAGA_PROJECT`
368
+ per repo and each agent sees only its own:
350
369
 
351
370
  ```json
352
371
  {
@@ -380,209 +399,212 @@ project was a guess, rather than silently reporting on the wrong repo.
380
399
  The web UI is unaffected either way: its project switcher lists every project in the database, and
381
400
  each tab is scoped to the selected one.
382
401
 
383
- ## Forgiving input
384
-
385
- Smaller models routinely send an array parameter as a *string* containing JSON.
386
- Every array-taking tool accepts that, so a batch does not silently collapse into one record:
387
-
388
- ```
389
- subtask_create({ task_id: 3, titles: '["Write it","Test it"]' }) # 2 subtasks
390
- subtask_create({ task_id: 3, titles: "- Write it
391
- - Test it" }) # 2 subtasks
392
- task_batch_update({ ids: "[4,5]", status: "done" }) # both tasks
393
- task_create({ epic_id: 1, title: "x", tags: "billing, urgent" }) # 2 tags
394
- ```
395
-
396
- Coercion stops where intent becomes ambiguous. A comma inside a *title* is left alone —
397
- `"Design the API, then implement it"` is one subtask, not two — while a comma in a tag or an id
398
- list is a separator, because neither can contain one. Anything genuinely unusable is refused with
399
- a message naming what arrived and what was wanted, rather than a leaked `ids.map is not a function`.
400
-
401
- ## Asking what to do next
402
+ ---
402
403
 
403
- `tracker_dashboard` hands an agent everything and leaves it to reason. `tracker_next` answers the
404
- question:
405
-
406
- ```
407
- tracker_next()
408
- -> Work on #12 'Write the adapter' — already in progress, high priority, in the
409
- active epic 'Provider swap'. Next step: implement. Also overdue: #18 'Renew cert'.
410
- 3 other task(s) are blocked.
411
- ```
412
-
413
- One recommendation with the reason, the next unfinished subtask inside it, a couple of
414
- alternatives, and anything overdue or blocked. About a third the size of the dashboard.
415
-
416
- The ordering rule worth knowing: **continuing beats starting.** A task already in progress outranks
417
- an untouched one that is overdue or higher priority, because abandoning work in flight just leaves
418
- two things unfinished — the overdue work is named in the summary instead. Blocked tasks are never
419
- recommended, archived epics and removed tasks are skipped, and subtask dependencies decide which
420
- step comes next inside the chosen task.
404
+ ## Web UI
421
405
 
422
- When nothing is actionable it says what to unblock rather than returning an empty answer:
406
+ Everything above is agent-facing. `saga-web` puts the same database in a browser — for the times
407
+ when reviewing a spec an agent just wrote, or fixing one field by hand, is faster than another
408
+ prompt.
423
409
 
410
+ ```bash
411
+ npx -p saga-mcp saga-web ./.tracker.db --open
424
412
  ```
425
- Nothing is actionable: all 4 remaining task(s) are blocked.
426
- Unblocking #7 'the keystone' would release 3 of them.
427
- ```
428
-
429
- ## Ordering and dependencies
430
413
 
431
- `task_list` sorts by priority by default, which is usually what an agent wants but ignores any
432
- order you arranged by hand. `sort_by: "manual"` reads back the order `task_reorder` set:
414
+ Or against a database you already point your MCP server at:
433
415
 
434
- ```
435
- task_reorder({ epic_id: 2, ordered_ids: [8, 5, 6] })
436
- task_list({ epic_id: 2, sort_by: "manual" }) # 8, 5, 6
416
+ ```bash
417
+ saga-web --db ~/saga/central.tracker.db --port 8080
437
418
  ```
438
419
 
439
- Anything omitted from `ordered_ids` keeps its relative position at the end. `sort_order` runs
440
- ascending — lower sorts first — and in the web UI you can drag tasks into place inside an epic.
420
+ | Option | Default | Description |
421
+ |--------|---------|-------------|
422
+ | `--db <path>` | `$DB_PATH` | Database to open. A positional path works too. |
423
+ | `--port <n>` | first free from `4319` | Omit it and saga-web takes the first free port, so one instance per project just works. `--port N` binds exactly N and fails if taken; `--port 0` lets the OS choose. Also `SAGA_WEB_PORT`. |
424
+ | `--host <addr>` | `127.0.0.1` | Bind address. Local-only by default. |
425
+ | `--read-only` | off | Serve the UI with every editing control removed. |
426
+ | `--open` | off | Open the UI in your default browser. |
441
427
 
442
- Task dependencies auto-block and auto-unblock:
428
+ Six tabs:
443
429
 
444
- ```
445
- task_update({ id: 9, depends_on: [8] }) # 9 becomes blocked while 8 is open
446
- ```
430
+ - **Overview** — stats, per-epic progress, blocked and overdue tasks
431
+ - **Board** — kanban across the five task statuses; drag a card to change its status
447
432
 
448
- Re-evaluation runs whenever a blocker's *doneness* changes in either direction, so reopening a
449
- finished blocker blocks its dependents again, and clearing the last dependency releases them.
450
- Circular dependencies are refused with the loop named, for tasks and subtasks alike — anything
451
- in a cycle would be blocked forever. The web UI shows a banner at the top of a blocked task
452
- naming what it waits on, with a picker to add or remove dependencies.
433
+ <picture>
434
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/board-dark.png">
435
+ <img alt="The board: five columns, one per task status" src="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/board-light.png">
436
+ </picture>
437
+
438
+ - **Epics** — the full Epic → Task → Subtask tree, which is the fastest way to review a spec an
439
+ agent just wrote. Blocked tasks carry a ⛔ naming what they wait on, finished ones are struck
440
+ through, and tasks drag into order. Epics read the same way — bold while in progress, struck
441
+ through once completed or cancelled — and each one archives from here as well as the Overview
442
+
443
+ <picture>
444
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/epics-dark.png">
445
+ <img alt="The epic tree, with epics and tasks struck through once finished and blocked tasks marked" src="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/epics-light.png">
446
+ </picture>
447
+
448
+ - **Notes** — decisions, context and blockers
449
+ - **Templates** — every template with the tasks it creates and the `{placeholders}` it uses; edit
450
+ the details, edit the task list, apply it to an epic, or delete it
451
+ - **Activity** — the complete change history
452
+
453
+ And throughout:
454
+
455
+ - **Task drawer** — edit any field, comment, remove or restore a comment, lock the description,
456
+ drag subtasks into order, and set which subtasks wait on which. Each subtask has one control
457
+ carrying its whole state (todo / in progress / done, or blocked), and the drawer resizes by
458
+ dragging its edge. Its Refresh/Edit/Close row stays pinned to the top however far you scroll,
459
+ as a long form keeps its Save button in view
460
+
461
+ <picture>
462
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/task-dark.png">
463
+ <img alt="The task drawer: subtasks with their state, dependencies both ways, and the comment thread" src="https://raw.githubusercontent.com/spranab/saga-mcp/master/docs/screenshots/task-light.png">
464
+ </picture>
465
+
466
+ - **Markdown** — descriptions, comments and notes render headings, tables, lists, code and links.
467
+ Agent-written content is escaped before any markdown rule runs, so raw HTML can never reach the
468
+ page, and only http/https/mailto links are followed
469
+ - **Project switcher** — every project in the database, so one central `.tracker.db` covers all
470
+ your repos; every tab, including Activity, is scoped to the selected project
471
+ - **Shareable, refreshable URLs** — the open project, tab and task live in the address bar, so a
472
+ browser refresh puts you back where you were and back/forward move between tasks. A ⟳ button in
473
+ the task drawer re-reads that task without a page reload, for picking up what an agent just wrote
453
474
 
454
- ## Getting old work out of the way
475
+ Writes from the UI call the *same handlers* the MCP tools do, so edits you make by hand are
476
+ validated identically and land in the same activity log as the agent's — an agent calling
477
+ `tracker_dashboard` after you fix something sees the fix and how it happened.
455
478
 
456
- An epic list that is mostly finished work, and tasks an agent created that should have been
457
- subtasks, are context you pay for on every call.
479
+ A few deliberate limits: it binds to `127.0.0.1` unless you ask otherwise, it has no authentication
480
+ (don't put it on a shared network), and it will not create a database — point it at one your MCP
481
+ server already uses. Separate `.tracker.db` files are not yet aggregated into one view; a single
482
+ database with multiple projects is.
458
483
 
459
- ```
460
- epic_archive({ id: 4 }) # the epic and its tasks drop out of listings
461
- task_delete({ id: 12, reason: "should have been a subtask" })
462
- ```
484
+ ---
463
485
 
464
- Archiving is deliberately **not** the `cancelled` status: `cancelled` means "we decided not to do
465
- this", while most of what you want to archive is *completed*. Archived epics and their tasks
466
- disappear from `epic_list`, `tracker_dashboard`, `task_list` and `tracker_search` — including the
467
- statistics, not just the lists — and come back with `include_archived`.
468
-
469
- Nothing vanishes silently. The dashboard says what it left out:
486
+ ## Token cost
470
487
 
471
- ```
472
- Hidden: 2 archived epic(s) and 1 removed task(s) — pass include_archived to include them.
473
- ```
488
+ The tool list is context every session pays before any work happens, and list responses are context
489
+ it pays again on every call. Both are kept deliberately small:
474
490
 
475
- `task_delete` is the same soft delete comments have, restricted to tasks still in `todo`:
476
- anything further along has comments, time tracking and an activity log that removing it would
477
- strand, and a task other tasks depend on is refused outright so nothing is left blocked forever.
478
- The row is kept, `task_restore` brings it back, and `tracker_export` includes archived and removed
479
- rows because a backup that omits things is not a backup.
491
+ - Responses are compact JSON — no pretty-print indentation, which measured 20-27% of every response
492
+ - `task_list` rows omit nulls and `metadata`, and truncate descriptions to 120 characters
493
+ (call `task_get` for a task's full text) — 19-39% smaller depending on how long your descriptions run
494
+ - `activity_log` omits null columns and the row id (no tool takes one) — about 27% smaller
495
+ - `tracker_search` returns previews rather than whole records — about 47% smaller; follow up with
496
+ `task_get` or `note_list` for the full text
497
+ - `SAGA_TOOLS=core` drops the listed surface from ~7,200 tokens to ~2,900
480
498
 
481
- ## Keeping agents on the rails
499
+ `note_list` deliberately keeps full note content — it is the retrieval tool, not a preview.
482
500
 
483
- Two guards for the ways an agent goes wrong on a long task.
501
+ Set `SAGA_TOOLS=core` when an agent only tracks work; leave it unset when you want templates,
502
+ import/export, session diffs and the rest discoverable. **Tools left off the list still work when
503
+ called by name** — `core` shrinks what is advertised, not what exists.
484
504
 
485
- **A locked description.** Agents sometimes rewrite a task's description to record progress, when
486
- they meant to add a comment — and the spec you agreed on is gone. Lock it and `task_update` refuses:
505
+ The core thirteen: `tracker_init`, `tracker_next`, `tracker_dashboard`, `project_list`,
506
+ `epic_create`, `epic_list`, `task_create`, `task_list`, `task_get`, `task_update`, `subtask_create`,
507
+ `note_save`, `comment_add`.
487
508
 
488
- ```
489
- task_lock_description({ id: 12 })
490
- task_update({ id: 12, description: "..." })
491
- -> Task 12's description is locked and was not changed. Record progress with
492
- comment_add instead, or unlock it in the web UI if the description is genuinely wrong.
493
- ```
509
+ Every tool description is held to a byte budget in the test suite, so the surface cannot grow by
510
+ accretion: adding a tool means trimming prose elsewhere or justifying the increase.
494
511
 
495
- Everything else about the task stays editable — the point is to protect the spec, not freeze the
496
- task. The lock cannot be cleared as a side effect of an ordinary `task_update`; it takes a
497
- deliberate `task_lock_description` call or the lock toggle in the web UI, and both are logged.
512
+ ---
498
513
 
499
- This is a guard against confusion, not an adversarial control: an agent that is told to unlock
500
- still can. It turns a silent overwrite into a visible, reversible decision.
514
+ ## Tool reference
501
515
 
502
- **Subtask order and dependencies.** New subtasks are appended in order rather than all landing at
503
- position 0, `subtask_reorder` sets the order in one call (or drag them in the UI), and a subtask
504
- can wait on its siblings:
516
+ ### Getting started
505
517
 
506
- ```
507
- subtask_update({ id: 8, depends_on: [5, 6] }) # 8 waits for 5 and 6
508
- subtask_update({ id: 4, blocks: [5, 6, 7, 8] }) # a bug that holds up the rest
509
- ```
518
+ | Tool | Description | Annotations |
519
+ |------|-------------|-------------|
520
+ | `tracker_init` | Initialize tracker and create first project | `readOnly: false`, `idempotent: true` |
521
+ | `tracker_next` | What to work on next, with the reason and what is blocked | `readOnly: true` |
522
+ | `tracker_dashboard` | Full project overview with natural language summary | `readOnly: true` |
510
523
 
511
- Reads carry `depends_on` and `blocked`, and the block is **enforced on write**: starting or
512
- finishing a subtask whose prerequisites are unmet is refused, and so is completing a task whose
513
- checklist is still open.
524
+ ### Projects
514
525
 
515
- ```
516
- subtask_update({ id: 8, status: "in_progress" })
517
- -> Subtask 8 cannot be started — it waits on #5 'write the parser' (todo).
518
- Finish those first, or pass force: true to override deliberately (the override is logged).
519
- ```
526
+ | Tool | Description | Annotations |
527
+ |------|-------------|-------------|
528
+ | `project_create` | Create a new project | `readOnly: false` |
529
+ | `project_list` | List projects with completion stats | `readOnly: true` |
530
+ | `project_update` | Update project (archive to soft-delete) | `readOnly: false`, `idempotent: true` |
520
531
 
521
- `force: true` is the way past, for when a person has decided the blocker no longer applies. It
522
- works on `subtask_update`, `task_update` and `task_batch_update`, and every override is written to
523
- the activity log naming what was skipped. The web UI asks for confirmation and then sends it.
532
+ ### Epics
524
533
 
525
- The distinction that matters is between an agent quietly ignoring a blocker and someone choosing
526
- to override one. Dependencies stay
527
- within one task — a checklist item waiting on something under a *different* task is a task-level
528
- dependency, and `task_update depends_on` already models that. Cycles are refused with the loop
529
- spelled out.
534
+ | Tool | Description | Annotations |
535
+ |------|-------------|-------------|
536
+ | `epic_create` | Create an epic within a project | `readOnly: false` |
537
+ | `epic_list` | List epics with task counts | `readOnly: true` |
538
+ | `epic_update` | Update an epic | `readOnly: false`, `idempotent: true` |
539
+ | `epic_archive` | Archive/unarchive an epic, hiding it and its tasks from listings | `readOnly: false`, `idempotent: true` |
530
540
 
531
- ## Web UI
541
+ ### Tasks
532
542
 
533
- Everything above is agent-facing. `saga-web` puts the same database in a browser — for the times
534
- when reviewing a spec an agent just wrote, or fixing one field by hand, is faster than another prompt.
543
+ | Tool | Description | Annotations |
544
+ |------|-------------|-------------|
545
+ | `task_create` | Create a task with optional dependencies | `readOnly: false` |
546
+ | `task_list` | List/filter tasks; follows a manual arrangement when one exists | `readOnly: true` |
547
+ | `task_get` | Get task with subtasks, notes, comments, and dependencies | `readOnly: true` |
548
+ | `task_update` | Update task (auto-logs, auto-blocks/unblocks) | `readOnly: false`, `idempotent: true` |
549
+ | `task_batch_update` | Update multiple tasks at once | `readOnly: false`, `idempotent: true` |
550
+ | `task_reorder` | Set the order of an epic's tasks | `readOnly: false`, `idempotent: true` |
551
+ | `task_lock_description` | Lock/unlock a description so agents can't rewrite it | `readOnly: false`, `idempotent: true` |
552
+ | `task_delete` | Remove a `todo` task (soft delete, restorable) | `readOnly: false`, `idempotent: true` |
553
+ | `task_restore` | Restore a removed task | `readOnly: false`, `idempotent: true` |
535
554
 
555
+ ### Subtasks
536
556
 
537
- The **Templates** tab lists every template with the tasks it will create, the
538
- `{placeholders}` it uses, and buttons to edit the details, edit the task list as
539
- JSON, apply it to an epic, or delete it. Templates live in the database as a
540
- whole rather than in one project, and the tab says so.
557
+ | Tool | Description | Annotations |
558
+ |------|-------------|-------------|
559
+ | `subtask_create` | Create subtask(s) — supports batch | `readOnly: false` |
560
+ | `subtask_update` | Update title/status/position; `depends_on` and `blocks` set ordering | `readOnly: false`, `idempotent: true` |
561
+ | `subtask_reorder` | Set the order of a task's subtasks in one call | `readOnly: false`, `idempotent: true` |
562
+ | `subtask_delete` | Delete subtask(s) — supports batch | `destructive: true`, `idempotent: true` |
541
563
 
542
- ```bash
543
- npx -p saga-mcp saga-web ./.tracker.db --open
544
- ```
564
+ ### Comments
545
565
 
546
- Or against a database you already point your MCP server at:
566
+ | Tool | Description | Annotations |
567
+ |------|-------------|-------------|
568
+ | `comment_add` | Add a comment to a task (threaded discussion) | `readOnly: false` |
569
+ | `comment_list` | List comments on a task (removed ones hidden unless `include_deleted`) | `readOnly: true` |
570
+ | `comment_delete` | Remove a comment — soft delete, row kept for audit | `readOnly: false`, `idempotent: true` |
571
+ | `comment_restore` | Restore a removed comment | `readOnly: false`, `idempotent: true` |
547
572
 
548
- ```bash
549
- saga-web --db ~/saga/central.tracker.db --port 8080
550
- ```
573
+ ### Templates
551
574
 
552
- | Option | Default | Description |
553
- |--------|---------|-------------|
554
- | `--db <path>` | `$DB_PATH` | Database to open. A positional path works too. |
555
- | `--port <n>` | first free from `4319` | Omit it and saga-web takes the first free port, so one instance per project just works. `--port N` binds exactly N and fails if taken; `--port 0` lets the OS choose. Also `SAGA_WEB_PORT`. |
556
- | `--host <addr>` | `127.0.0.1` | Bind address. Local-only by default. |
557
- | `--read-only` | off | Serve the UI with every editing control removed. |
558
- | `--open` | off | Open the UI in your default browser. |
575
+ | Tool | Description | Annotations |
576
+ |------|-------------|-------------|
577
+ | `template_create` | Create a reusable task template with `{variable}` placeholders | `readOnly: false` |
578
+ | `template_list` | List templates; `include_tasks` shows what each one creates | `readOnly: true` |
579
+ | `template_update` | Edit a template in place — name, description or tasks | `readOnly: false`, `idempotent: true` |
580
+ | `template_apply` | Apply template to create tasks with variable substitution | `readOnly: false` |
581
+ | `template_delete` | Delete a template | `destructive: true`, `idempotent: true` |
559
582
 
560
- What you get:
583
+ ### Notes
561
584
 
562
- - **Overview** — stats, per-epic progress, blocked and overdue tasks
563
- - **Board** — kanban across the five task statuses; drag a card to change its status
564
- - **Epics** — the full Epic → Task → Subtask tree, which is the fastest way to review a spec an agent just wrote
565
- - **Notes** and **Activity** — decisions and the complete change history
566
- - **Markdown** — descriptions, comments and notes render headings, tables, lists, code and links. Agent-written content is escaped before any markdown rule runs, so raw HTML can never reach the page, and only http/https/mailto links are followed
567
- - **Archived section** — archived epics collapse below a divider, with a "show archived (N)" toggle
568
- - **Task drawer** — edit any field, comment, remove or restore a comment, lock the description, drag subtasks into order, and set which subtasks wait on which. Each subtask has one control carrying its whole state (todo / in progress / done, or blocked), and the drawer resizes by dragging its edge
569
- - **Project switcher** — every project in the database, so one central `.tracker.db` covers all your repos; every tab, including Activity, is scoped to the selected project
570
- - **Shareable, refreshable URLs** — the open project, tab and task live in the address bar, so a browser refresh puts you back where you were and back/forward move between tasks. A ⟳ button in the task drawer re-reads that task without a page reload, for picking up what an agent just wrote
585
+ | Tool | Description | Annotations |
586
+ |------|-------------|-------------|
587
+ | `note_save` | Create or update a note (upsert) | `readOnly: false` |
588
+ | `note_list` | List notes with filters | `readOnly: true` |
589
+ | `note_search` | Full-text search across notes | `readOnly: true` |
590
+ | `note_delete` | Delete a note | `destructive: true`, `idempotent: true` |
571
591
 
572
- Writes from the UI call the *same handlers* the MCP tools do, so edits you make by hand are
573
- validated identically and land in the same activity log as the agent's — an agent calling
574
- `tracker_dashboard` after you fix something sees the fix and how it happened.
592
+ ### Search, history and transfer
575
593
 
576
- A few deliberate limits: it binds to `127.0.0.1` unless you ask otherwise, it has no
577
- authentication (don't put it on a shared network), and it will not create a database — point it
578
- at one your MCP server already uses. Separate `.tracker.db` files are not yet aggregated into
579
- one view; a single database with multiple projects is.
594
+ | Tool | Description | Annotations |
595
+ |------|-------------|-------------|
596
+ | `tracker_search` | Cross-entity search (projects, epics, tasks, notes) | `readOnly: true` |
597
+ | `activity_log` | View change history with filters | `readOnly: true` |
598
+ | `tracker_session_diff` | What changed since a timestamp — call at session start | `readOnly: true` |
599
+ | `tracker_export` | Export full project as nested JSON (includes dependencies and comments) | `readOnly: true` |
600
+ | `tracker_import` | Import project from JSON (matching export format) | `readOnly: false` |
580
601
 
581
- ## How It Works
602
+ ---
582
603
 
583
- saga-mcp stores everything in a single SQLite file (`.tracker.db`) per project. The database is auto-created on first use with all tables and indexes — no migration step needed.
604
+ ## How it works
584
605
 
585
- ### Hierarchy
606
+ Everything lives in a single SQLite file. The schema is created on first use, and existing
607
+ databases are migrated in place when you upgrade — there is no migration step to run.
586
608
 
587
609
  ```
588
610
  Project
@@ -593,14 +615,7 @@ Project
593
615
  └── Dependencies (blocked by other tasks)
594
616
  ```
595
617
 
596
- ### Task Dependencies
597
-
598
- Tasks can depend on other tasks. When you set `depends_on: [2, 3]` on a task:
599
- - The task is auto-blocked if any dependency isn't `done`
600
- - When a dependency is marked `done`, downstream tasks are re-evaluated
601
- - If all dependencies are met, the blocked task auto-unblocks to `todo`
602
-
603
- ### Note Types
618
+ ### Note types
604
619
 
605
620
  Notes replace scattered markdown files. Each note has a type:
606
621
 
@@ -615,9 +630,9 @@ Notes replace scattered markdown files. Each note has a type:
615
630
  | `progress` | Progress updates |
616
631
  | `release` | Release notes |
617
632
 
618
- ### Activity Log
633
+ ### Activity log
619
634
 
620
- Every create, update, and delete is automatically recorded:
635
+ Every create, update and delete is recorded, with the old and new value:
621
636
 
622
637
  ```json
623
638
  {
@@ -632,18 +647,21 @@ Every create, update, and delete is automatically recorded:
632
647
  }
633
648
  ```
634
649
 
635
- ## Privacy Policy
650
+ That log is what makes the soft deletes safe and the time tracking automatic — hours are computed
651
+ from it rather than entered by hand.
652
+
653
+ ---
636
654
 
637
- saga-mcp is a fully local, offline tool. It does **not**:
655
+ ## Privacy
638
656
 
639
- - Collect any user data
640
- - Send any data to external servers
641
- - Require internet access after installation
642
- - Use analytics, telemetry, or tracking of any kind
657
+ saga-mcp is a fully local, offline tool. It does **not** collect user data, send anything to
658
+ external servers, require internet access after installation, or use analytics or telemetry of any
659
+ kind.
643
660
 
644
- All data is stored exclusively in the local SQLite file specified by `DB_PATH`. You own your data completely. Uninstalling saga-mcp and deleting the `.tracker.db` file removes all traces.
661
+ All data is stored exclusively in the local SQLite file specified by `DB_PATH`. Uninstalling
662
+ saga-mcp and deleting the `.tracker.db` file removes all traces.
645
663
 
646
- For questions about privacy, open an issue at https://github.com/spranab/saga-mcp/issues.
664
+ ---
647
665
 
648
666
  ## Development
649
667
 
@@ -657,10 +675,14 @@ DB_PATH=./test.db npm start
657
675
  # the web UI against the same database
658
676
  node dist/web/index.js ./test.db --open
659
677
 
660
- npm test # unit and integration, ~140 tests, no network
678
+ npm test # 358 unit and integration tests, no network
661
679
  npm run e2e # release gate: packs a tarball, installs it, drives the real binaries
662
680
  ```
663
681
 
682
+ `npm test` runs against the built output. `npm run e2e` is the gate that matters before a release:
683
+ it packs the tarball that would actually be published, installs it somewhere else, and drives both
684
+ binaries over real stdio — 106 checks, including an upgrade from an older database.
685
+
664
686
  ### Releasing
665
687
 
666
688
  Publishing to npm is irreversible — a version number can never be reused — so it is the *last*
@@ -669,41 +691,45 @@ step, and it is triggered by publishing a GitHub release, not by pushing a tag.
669
691
  ```bash
670
692
  # 1. bump the version in package.json, manifest.json and server.json, then merge
671
693
  # 2. tag it. Nothing is published yet.
672
- git tag -a v1.9.0 -m "v1.9.0 — ..." && git push origin v1.9.0
694
+ git tag -a v1.17.0 -m "v1.17.0 — ..." && git push origin v1.17.0
673
695
 
674
696
  # 3. verify the tagged build: this packs the tarball that would be published
675
697
  # and drives it end to end, including an upgrade from an older database.
676
698
  npm run e2e
677
699
 
678
700
  # 4. publish the release. This fires the publish workflow.
679
- gh release create v1.9.0 --notes-file notes.md
701
+ gh release create v1.17.0 --notes-file notes.md
680
702
  ```
681
703
 
682
704
  The workflow re-runs the suite against the tagged commit, refuses a tag that does not match
683
705
  `package.json`, refuses a version already on npm, and sends a GitHub *pre-release* to the `next`
684
706
  dist-tag so it never becomes what `npm install saga-mcp` gives people. A failed publish can be
685
- retried against the same tag with `gh workflow run "Publish to npm" -f tag=v1.9.0`.
707
+ retried against the same tag with `gh workflow run "Publish to npm" -f tag=v1.17.0`.
708
+
709
+ ---
686
710
 
687
711
  ## Support
688
712
 
689
713
  - **Issues**: https://github.com/spranab/saga-mcp/issues
690
714
  - **Repository**: https://github.com/spranab/saga-mcp
691
715
 
716
+ Bug reports that come with a reproduction are worth a great deal here — several of the sharper
717
+ behaviours above exist because someone reported that the obvious thing was wrong.
718
+
692
719
  ## Related projects
693
720
 
694
- Part of a set of agent infrastructure built by one person, meant to be used
695
- together:
696
-
697
- - [yantrikdb-mcp](https://github.com/yantrikos/yantrikdb-mcp) — persistent
698
- cognitive memory for the same agent: what it learned, not what it planned.
699
- - [brainstorm-mcp](https://github.com/spranab/brainstorm-mcp) — multi-model
700
- debate before you commit a plan to the tracker.
701
- - [swarmcode](https://github.com/spranab/swarmcode) — real-time channel
702
- between Claude Code instances on different machines.
703
- - [truenas-mcp](https://github.com/spranab/truenas-mcp) — 278 TrueNAS SCALE
704
- actions behind one hierarchical tool.
705
- - [mcpier](https://github.com/spranab/mcpier) — self-hosted MCP control plane
706
- that keeps API keys off your clients.
721
+ Part of a set of agent infrastructure built by one person, meant to be used together:
722
+
723
+ - [yantrikdb-mcp](https://github.com/yantrikos/yantrikdb-mcp) — persistent cognitive memory for the
724
+ same agent: what it learned, not what it planned.
725
+ - [brainstorm-mcp](https://github.com/spranab/brainstorm-mcp) — multi-model debate before you commit
726
+ a plan to the tracker.
727
+ - [swarmcode](https://github.com/spranab/swarmcode) — real-time channel between Claude Code
728
+ instances on different machines.
729
+ - [truenas-mcp](https://github.com/spranab/truenas-mcp) — 278 TrueNAS SCALE actions behind one
730
+ hierarchical tool.
731
+ - [mcpier](https://github.com/spranab/mcpier) — self-hosted MCP control plane that keeps API keys
732
+ off your clients.
707
733
 
708
734
  ## License
709
735