● Zion Boggan zionboggan.com ↗

Add mark_list MCP tool

The MCP server could start and check individual marks by name but had no
way to enumerate existing marks. Agents that cannot recall a mark name
have no recovery path short of guessing.

Wire service.ListMarks into the MCP handler as the mark_list tool and
add tests covering the empty case, listing multiple marks, and verifying
deleted marks are excluded.
17ac44c   Zion Boggan committed on Jun 29, 2026 (1 month ago)
README.md +1 -0
@@ -81,6 +81,7 @@ JSON responses include a `confidence` value. For v0.1, persisted elapsed calcula
- `session_status`
- `mark_start`
- `mark_elapsed`
+- `mark_list`
- `stale_check`
- `ledger_event`
- `ledger_report`
internal/ledger/ledger_test.go +60 -0
@@ -123,6 +123,66 @@ func TestStatePersistence(t *testing.T) {
}
}
+func TestListMarksEmpty(t *testing.T) {
+ service := testService(t, time.Date(2026, 6, 19, 6, 30, 0, 0, time.UTC))
+ marks, err := service.ListMarks()
+ if err != nil {
+ t.Fatalf("ListMarks returned error: %v", err)
+ }
+ if len(marks) != 0 {
+ t.Fatalf("expected 0 marks, got %d", len(marks))
+ }
+}
+
+func TestListMarksReturnsAll(t *testing.T) {
+ now := time.Date(2026, 6, 19, 6, 30, 0, 0, time.UTC)
+ service := testService(t, now)
+ if _, err := service.StartMark("build"); err != nil {
+ t.Fatalf("StartMark(build) returned error: %v", err)
+ }
+ if _, err := service.StartMark("test"); err != nil {
+ t.Fatalf("StartMark(test) returned error: %v", err)
+ }
+ marks, err := service.ListMarks()
+ if err != nil {
+ t.Fatalf("ListMarks returned error: %v", err)
+ }
+ if len(marks) != 2 {
+ t.Fatalf("expected 2 marks, got %d", len(marks))
+ }
+ names := map[string]struct{}{}
+ for _, m := range marks {
+ names[m.Name] = struct{}{}
+ }
+ if _, ok := names["build"]; !ok {
+ t.Fatal("mark 'build' not in ListMarks result")
+ }
+ if _, ok := names["test"]; !ok {
+ t.Fatal("mark 'test' not in ListMarks result")
+ }
+}
+
+func TestListMarksExcludesDeleted(t *testing.T) {
+ now := time.Date(2026, 6, 19, 6, 30, 0, 0, time.UTC)
+ service := testService(t, now)
+ if _, err := service.StartMark("build"); err != nil {
+ t.Fatalf("StartMark returned error: %v", err)
+ }
+ if _, err := service.StartMark("deploy"); err != nil {
+ t.Fatalf("StartMark returned error: %v", err)
+ }
+ if err := service.DeleteMark("deploy"); err != nil {
+ t.Fatalf("DeleteMark returned error: %v", err)
+ }
+ marks, err := service.ListMarks()
+ if err != nil {
+ t.Fatalf("ListMarks returned error: %v", err)
+ }
+ if len(marks) != 1 || marks[0].Name != "build" {
+ t.Fatalf("expected [build], got %v", marks)
+ }
+}
+
func TestJSONLEventValidity(t *testing.T) {
dir := t.TempDir()
service := NewService(state.NewStore(dir))
internal/mcp/mcp.go +3 -0
@@ -158,6 +158,8 @@ func handleToolCall(raw json.RawMessage, service *ledger.Service) (callResult, e
result, err = service.MarkElapsed(requiredString(args, "name"))
case "stale_check":
result, err = staleCheck(args, service)
+ case "mark_list":
+ result, err = service.ListMarks()
case "ledger_event":
note, _ := args["note"].(string)
err = service.LedgerEvent(note)
@@ -211,6 +213,7 @@ func tools() []map[string]any {
tool("mark_elapsed", "Return elapsed time for a named mark.", map[string]any{
"name": stringSchema("Mark name"),
}, "name"),
+ tool("mark_list", "Return all open marks with elapsed time.", map[string]any{}),
tool("stale_check", "Check whether a timestamp or mark is stale for a TTL.", map[string]any{
"timestamp": stringSchema("RFC3339 timestamp"),
"mark": stringSchema("Existing mark name"),