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 +449 -433
- package/dist/helpers/description-lock.d.ts +32 -0
- package/dist/helpers/description-lock.js +53 -0
- package/dist/helpers/description-lock.js.map +1 -0
- package/dist/helpers/json-columns.d.ts +22 -0
- package/dist/helpers/json-columns.js +59 -0
- package/dist/helpers/json-columns.js.map +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/activity.js +5 -0
- package/dist/tools/activity.js.map +1 -1
- package/dist/tools/export-import.js +17 -10
- package/dist/tools/export-import.js.map +1 -1
- package/dist/tools/tasks.js +11 -0
- package/dist/tools/tasks.js.map +1 -1
- package/dist/web/index.js +2 -1
- package/dist/web/index.js.map +1 -1
- package/dist/web/queries.js +4 -1
- package/dist/web/queries.js.map +1 -1
- package/dist/web/ui.js +103 -20
- package/dist/web/ui.js.map +1 -1
- package/manifest.json +18 -4
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -5,20 +5,21 @@
|
|
|
5
5
|
[](https://github.com/spranab/saga-mcp/blob/master/LICENSE)
|
|
6
6
|
[](https://ideacred.com/profile/spranab)
|
|
7
7
|
|
|
8
|
-
Your coding agent loses the plan between sessions. You come back tomorrow and
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
17
|
+
---
|
|
20
18
|
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
recent activity,
|
|
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
|
-
|
|
81
|
+
And when you would rather look than ask, `saga-web` puts the same database in a browser:
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
89
|
+
---
|
|
88
90
|
|
|
89
|
-
|
|
91
|
+
## What you get
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
111
|
+
---
|
|
106
112
|
|
|
107
|
-
|
|
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
|
-
|
|
115
|
+
`tracker_dashboard` hands an agent everything and leaves it to reason. `tracker_next` answers the
|
|
116
|
+
question:
|
|
124
117
|
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
134
|
+
When nothing is actionable it says what to unblock rather than returning an empty answer:
|
|
141
135
|
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
context it pays again on every call. Both are kept deliberately small:
|
|
141
|
+
---
|
|
146
142
|
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
165
|
+
Task dependencies auto-block and auto-unblock:
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
+
## Keeping agents on the rails
|
|
188
180
|
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
+
---
|
|
256
242
|
|
|
257
|
-
|
|
243
|
+
## Comments as a decision trail
|
|
258
244
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
251
|
+
Comments persist across sessions — next time an agent calls `task_get(5)`, it sees the full thread.
|
|
269
252
|
|
|
270
|
-
|
|
253
|
+
If a comment turns out to be wrong, retract it without losing the trail:
|
|
271
254
|
|
|
272
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
263
|
+
---
|
|
283
264
|
|
|
284
|
-
|
|
265
|
+
## Templates
|
|
285
266
|
|
|
286
|
-
|
|
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
|
-
{
|
|
293
|
-
{
|
|
294
|
-
{
|
|
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
|
-
|
|
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
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
template_update({ id: 1,
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
+
Nothing vanishes silently. The dashboard says what it left out:
|
|
322
324
|
|
|
323
325
|
```
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
+
---
|
|
332
336
|
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
`
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
445
|
-
explicit `sort_by` is always obeyed literally.
|
|
428
|
+
Six tabs:
|
|
446
429
|
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
484
|
+
---
|
|
465
485
|
|
|
466
|
-
|
|
467
|
-
subtasks, are context you pay for on every call.
|
|
486
|
+
## Token cost
|
|
468
487
|
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
499
|
+
`note_list` deliberately keeps full note content — it is the retrieval tool, not a preview.
|
|
480
500
|
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
512
|
+
---
|
|
494
513
|
|
|
495
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
541
|
+
### Tasks
|
|
542
542
|
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
553
|
-
npx -p saga-mcp saga-web ./.tracker.db --open
|
|
554
|
-
```
|
|
564
|
+
### Comments
|
|
555
565
|
|
|
556
|
-
|
|
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
|
-
|
|
559
|
-
saga-web --db ~/saga/central.tracker.db --port 8080
|
|
560
|
-
```
|
|
573
|
+
### Templates
|
|
561
574
|
|
|
562
|
-
|
|
|
563
|
-
|
|
564
|
-
|
|
|
565
|
-
|
|
|
566
|
-
|
|
|
567
|
-
|
|
|
568
|
-
|
|
|
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
|
-
|
|
583
|
+
### Notes
|
|
571
584
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
602
|
+
---
|
|
592
603
|
|
|
593
|
-
|
|
604
|
+
## How it works
|
|
594
605
|
|
|
595
|
-
|
|
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
|
-
###
|
|
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
|
|
633
|
+
### Activity log
|
|
629
634
|
|
|
630
|
-
Every create, update
|
|
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
|
-
|
|
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
|
-
|
|
655
|
+
## Privacy
|
|
648
656
|
|
|
649
|
-
-
|
|
650
|
-
|
|
651
|
-
|
|
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`.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|