saga-mcp 1.16.0 → 1.17.1

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.
52
+
53
+ ---
37
54
 
38
- ## What it looks like
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
 
55
- ```
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)."
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.
60
76
  ```
61
77
 
62
- Plus the structured data behind it: stats, epics, blocked and overdue tasks,
63
- recent activity, notes.
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.
64
80
 
65
- ## Features
81
+ And when you would rather look than ask, `saga-web` puts the same database in a browser:
66
82
 
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
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>
84
87
 
85
- ## Other clients
86
88
 
87
- ### Claude Code
89
+ ---
88
90
 
89
- Add to your project's `.mcp.json`:
91
+ ## What you get
90
92
 
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
- ```
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
104
110
 
105
- ### With Claude Desktop
111
+ ---
106
112
 
107
- Add to your Claude Desktop config (`claude_desktop_config.json`):
108
-
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>
302
+
303
+
304
+ ---
305
+
306
+ ## Getting old work out of the way
311
307
 
312
- // or replace the task list wholesale
313
- template_update({ id: 1, tasks: [{ title: "Design {feature}", priority: "high" }] })
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.
314
310
 
315
- // see what a template actually creates, rather than just how many tasks
316
- template_list({ include_tasks: true })
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,219 +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
-
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.
402
+ ---
415
403
 
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
- **A deliberate order wins over a guess.** `task_list` sorts by priority until someone arranges an
432
- epic, and from then on it follows the arrangement:
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 }) # 8, 5, 6 — the plan, in order
437
- task_list({ epic_id: 2, sort_by: "priority" }) # priority, if that is what you want
416
+ ```bash
417
+ saga-web --db ~/saga/central.tracker.db --port 8080
438
418
  ```
439
419
 
440
- Priority is a reasonable guess about what matters; a sequence someone wrote down is not a guess.
441
- An agent handed a plan should start at the beginning of it, not at whichever step happens to be
442
- marked critical.
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. |
443
427
 
444
- Nothing changes for epics nobody has arranged — those sort by priority exactly as before, and an
445
- explicit `sort_by` is always obeyed literally.
428
+ Six tabs:
446
429
 
447
- Anything omitted from `ordered_ids` keeps its relative position at the end. `sort_order` runs
448
- ascending — lower sorts first — and a task created *after* an arrangement has no place in it, so it
449
- lands at the end rather than the front. In the web UI you can drag tasks into place inside an epic,
450
- and finished ones are struck through.
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
451
432
 
452
- Task dependencies auto-block and auto-unblock:
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
- ```
455
- task_update({ id: 9, depends_on: [8] }) # 9 becomes blocked while 8 is open
456
- ```
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.
457
478
 
458
- Re-evaluation runs whenever a blocker's *doneness* changes in either direction, so reopening a
459
- finished blocker blocks its dependents again, and clearing the last dependency releases them.
460
- Circular dependencies are refused with the loop named, for tasks and subtasks alike — anything
461
- in a cycle would be blocked forever. The web UI shows a banner at the top of a blocked task
462
- naming what it waits on, with a picker to add or remove dependencies.
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.
463
483
 
464
- ## Getting old work out of the way
484
+ ---
465
485
 
466
- An epic list that is mostly finished work, and tasks an agent created that should have been
467
- subtasks, are context you pay for on every call.
486
+ ## Token cost
468
487
 
469
- ```
470
- epic_archive({ id: 4 }) # the epic and its tasks drop out of listings
471
- task_delete({ id: 12, reason: "should have been a subtask" })
472
- ```
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:
473
490
 
474
- Archiving is deliberately **not** the `cancelled` status: `cancelled` means "we decided not to do
475
- this", while most of what you want to archive is *completed*. Archived epics and their tasks
476
- disappear from `epic_list`, `tracker_dashboard`, `task_list` and `tracker_search` — including the
477
- statistics, not just the lists — and come back with `include_archived`.
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
478
498
 
479
- Nothing vanishes silently. The dashboard says what it left out:
499
+ `note_list` deliberately keeps full note content — it is the retrieval tool, not a preview.
480
500
 
481
- ```
482
- Hidden: 2 archived epic(s) and 1 removed task(s) — pass include_archived to include them.
483
- ```
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
- `task_delete` is the same soft delete comments have, restricted to tasks still in `todo`:
486
- anything further along has comments, time tracking and an activity log that removing it would
487
- strand, and a task other tasks depend on is refused outright so nothing is left blocked forever.
488
- The row is kept, `task_restore` brings it back, and `tracker_export` includes archived and removed
489
- rows because a backup that omits things is not a backup.
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`.
490
508
 
491
- ## Keeping agents on the rails
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.
492
511
 
493
- Two guards for the ways an agent goes wrong on a long task.
512
+ ---
494
513
 
495
- **A locked description.** Agents sometimes rewrite a task's description to record progress, when
496
- they meant to add a comment — and the spec you agreed on is gone. Lock it and `task_update` refuses:
514
+ ## Tool reference
497
515
 
498
- ```
499
- task_lock_description({ id: 12 })
500
- task_update({ id: 12, description: "..." })
501
- -> Task 12's description is locked and was not changed. Record progress with
502
- comment_add instead, or unlock it in the web UI if the description is genuinely wrong.
503
- ```
516
+ ### Getting started
504
517
 
505
- Everything else about the task stays editable — the point is to protect the spec, not freeze the
506
- task. The lock cannot be cleared as a side effect of an ordinary `task_update`; it takes a
507
- deliberate `task_lock_description` call or the lock toggle in the web UI, and both are logged.
508
-
509
- This is a guard against confusion, not an adversarial control: an agent that is told to unlock
510
- still can. It turns a silent overwrite into a visible, reversible decision.
511
-
512
- **Subtask order and dependencies.** New subtasks are appended in order rather than all landing at
513
- position 0, `subtask_reorder` sets the order in one call (or drag them in the UI), and a subtask
514
- can wait on its siblings:
515
-
516
- ```
517
- subtask_update({ id: 8, depends_on: [5, 6] }) # 8 waits for 5 and 6
518
- subtask_update({ id: 4, blocks: [5, 6, 7, 8] }) # a bug that holds up the rest
519
- ```
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` |
520
523
 
521
- Reads carry `depends_on` and `blocked`, and the block is **enforced on write**: starting or
522
- finishing a subtask whose prerequisites are unmet is refused, and so is completing a task whose
523
- checklist is still open.
524
+ ### Projects
524
525
 
525
- ```
526
- subtask_update({ id: 8, status: "in_progress" })
527
- -> Subtask 8 cannot be started — it waits on #5 'write the parser' (todo).
528
- Finish those first, or pass force: true to override deliberately (the override is logged).
529
- ```
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` |
530
531
 
531
- `force: true` is the way past, for when a person has decided the blocker no longer applies. It
532
- works on `subtask_update`, `task_update` and `task_batch_update`, and every override is written to
533
- the activity log naming what was skipped. The web UI asks for confirmation and then sends it.
532
+ ### Epics
534
533
 
535
- The distinction that matters is between an agent quietly ignoring a blocker and someone choosing
536
- to override one. Dependencies stay
537
- within one task — a checklist item waiting on something under a *different* task is a task-level
538
- dependency, and `task_update depends_on` already models that. Cycles are refused with the loop
539
- 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` |
540
540
 
541
- ## Web UI
541
+ ### Tasks
542
542
 
543
- Everything above is agent-facing. `saga-web` puts the same database in a browser — for the times
544
- 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` |
545
554
 
555
+ ### Subtasks
546
556
 
547
- The **Templates** tab lists every template with the tasks it will create, the
548
- `{placeholders}` it uses, and buttons to edit the details, edit the task list as
549
- JSON, apply it to an epic, or delete it. Templates live in the database as a
550
- 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` |
551
563
 
552
- ```bash
553
- npx -p saga-mcp saga-web ./.tracker.db --open
554
- ```
564
+ ### Comments
555
565
 
556
- 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` |
557
572
 
558
- ```bash
559
- saga-web --db ~/saga/central.tracker.db --port 8080
560
- ```
573
+ ### Templates
561
574
 
562
- | Option | Default | Description |
563
- |--------|---------|-------------|
564
- | `--db <path>` | `$DB_PATH` | Database to open. A positional path works too. |
565
- | `--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`. |
566
- | `--host <addr>` | `127.0.0.1` | Bind address. Local-only by default. |
567
- | `--read-only` | off | Serve the UI with every editing control removed. |
568
- | `--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` |
569
582
 
570
- What you get:
583
+ ### Notes
571
584
 
572
- - **Overview** — stats, per-epic progress, blocked and overdue tasks
573
- - **Board** — kanban across the five task statuses; drag a card to change its status
574
- - **Epics** — the full Epic → Task → Subtask tree, which is the fastest way to review a spec an agent just wrote
575
- - **Notes** and **Activity** — decisions and the complete change history
576
- - **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
577
- - **Archived section** — archived epics collapse below a divider, with a "show archived (N)" toggle
578
- - **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
579
- - **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
580
- - **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` |
581
591
 
582
- Writes from the UI call the *same handlers* the MCP tools do, so edits you make by hand are
583
- validated identically and land in the same activity log as the agent's — an agent calling
584
- `tracker_dashboard` after you fix something sees the fix and how it happened.
592
+ ### Search, history and transfer
585
593
 
586
- A few deliberate limits: it binds to `127.0.0.1` unless you ask otherwise, it has no
587
- authentication (don't put it on a shared network), and it will not create a database — point it
588
- at one your MCP server already uses. Separate `.tracker.db` files are not yet aggregated into
589
- 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` |
590
601
 
591
- ## How It Works
602
+ ---
592
603
 
593
- 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
594
605
 
595
- ### 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.
596
608
 
597
609
  ```
598
610
  Project
@@ -603,14 +615,7 @@ Project
603
615
  └── Dependencies (blocked by other tasks)
604
616
  ```
605
617
 
606
- ### Task Dependencies
607
-
608
- Tasks can depend on other tasks. When you set `depends_on: [2, 3]` on a task:
609
- - The task is auto-blocked if any dependency isn't `done`
610
- - When a dependency is marked `done`, downstream tasks are re-evaluated
611
- - If all dependencies are met, the blocked task auto-unblocks to `todo`
612
-
613
- ### Note Types
618
+ ### Note types
614
619
 
615
620
  Notes replace scattered markdown files. Each note has a type:
616
621
 
@@ -625,9 +630,9 @@ Notes replace scattered markdown files. Each note has a type:
625
630
  | `progress` | Progress updates |
626
631
  | `release` | Release notes |
627
632
 
628
- ### Activity Log
633
+ ### Activity log
629
634
 
630
- Every create, update, and delete is automatically recorded:
635
+ Every create, update and delete is recorded, with the old and new value:
631
636
 
632
637
  ```json
633
638
  {
@@ -642,18 +647,21 @@ Every create, update, and delete is automatically recorded:
642
647
  }
643
648
  ```
644
649
 
645
- ## 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
+ ---
646
654
 
647
- saga-mcp is a fully local, offline tool. It does **not**:
655
+ ## Privacy
648
656
 
649
- - Collect any user data
650
- - Send any data to external servers
651
- - Require internet access after installation
652
- - 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.
653
660
 
654
- 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.
655
663
 
656
- For questions about privacy, open an issue at https://github.com/spranab/saga-mcp/issues.
664
+ ---
657
665
 
658
666
  ## Development
659
667
 
@@ -667,10 +675,14 @@ DB_PATH=./test.db npm start
667
675
  # the web UI against the same database
668
676
  node dist/web/index.js ./test.db --open
669
677
 
670
- npm test # unit and integration, ~140 tests, no network
678
+ npm test # 359 unit and integration tests, no network
671
679
  npm run e2e # release gate: packs a tarball, installs it, drives the real binaries
672
680
  ```
673
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
+
674
686
  ### Releasing
675
687
 
676
688
  Publishing to npm is irreversible — a version number can never be reused — so it is the *last*
@@ -679,41 +691,45 @@ step, and it is triggered by publishing a GitHub release, not by pushing a tag.
679
691
  ```bash
680
692
  # 1. bump the version in package.json, manifest.json and server.json, then merge
681
693
  # 2. tag it. Nothing is published yet.
682
- git tag -a v1.9.0 -m "v1.9.0 — ..." && git push origin v1.9.0
694
+ git tag -a v1.17.1 -m "v1.17.1 — ..." && git push origin v1.17.1
683
695
 
684
696
  # 3. verify the tagged build: this packs the tarball that would be published
685
697
  # and drives it end to end, including an upgrade from an older database.
686
698
  npm run e2e
687
699
 
688
700
  # 4. publish the release. This fires the publish workflow.
689
- gh release create v1.9.0 --notes-file notes.md
701
+ gh release create v1.17.1 --notes-file notes.md
690
702
  ```
691
703
 
692
704
  The workflow re-runs the suite against the tagged commit, refuses a tag that does not match
693
705
  `package.json`, refuses a version already on npm, and sends a GitHub *pre-release* to the `next`
694
706
  dist-tag so it never becomes what `npm install saga-mcp` gives people. A failed publish can be
695
- 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.1`.
708
+
709
+ ---
696
710
 
697
711
  ## Support
698
712
 
699
713
  - **Issues**: https://github.com/spranab/saga-mcp/issues
700
714
  - **Repository**: https://github.com/spranab/saga-mcp
701
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
+
702
719
  ## Related projects
703
720
 
704
- Part of a set of agent infrastructure built by one person, meant to be used
705
- together:
706
-
707
- - [yantrikdb-mcp](https://github.com/yantrikos/yantrikdb-mcp) — persistent
708
- cognitive memory for the same agent: what it learned, not what it planned.
709
- - [brainstorm-mcp](https://github.com/spranab/brainstorm-mcp) — multi-model
710
- debate before you commit a plan to the tracker.
711
- - [swarmcode](https://github.com/spranab/swarmcode) — real-time channel
712
- between Claude Code instances on different machines.
713
- - [truenas-mcp](https://github.com/spranab/truenas-mcp) — 278 TrueNAS SCALE
714
- actions behind one hierarchical tool.
715
- - [mcpier](https://github.com/spranab/mcpier) — self-hosted MCP control plane
716
- 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.
717
733
 
718
734
  ## License
719
735