Skip to main content
Glama
README.md
# GoalTrack — Generic Daily Goal Tracker (MCP Server)

**The problem it solves:** most daily-goal tracking apps are built for one
metric — a water app, a step counter, a screen-time limiter — even though
the underlying pattern is identical: log amounts throughout the day, check
them against a target, and track whether you're keeping a streak. GoalTrack
lets an LLM (Claude, or any MCP client) manage *any* number of goals at
once — water, steps, pages read, screen time, calories, whatever — and get
timezone-correct progress and streaks for each.

## What it does

| Tool | Purpose |
| --- | --- |
| `add_goal` | Create a goal (name, unit, target, direction, threshold, timezone) |
| `edit_goal` | Update any field on an existing goal |
| `delete_goal` | Soft-delete (default, keeps history) or hard-delete a goal |
| `list_goals` | List all goals with today's progress inline |
| `log_entry` | Log an amount against a goal |
| `edit_entry` | Fix a mislogged entry's amount |
| `delete_entry` | Remove a logged entry |
| `get_progress` | Today's total vs target for one goal, with % complete |
| `history` | Daily totals + met/missed status over the last N days |
| `streak_status` | Current streak + longest streak for a goal |

Plus a `goaltrack://goal-templates` resource (a small library of common goal
presets an LLM can suggest — Water, Steps, Screen Time, etc.) and a
`daily_encouragement_prompt` prompt template that asks the assistant to
write a short, warm nudge about today's progress.

### Direction: `at_least` vs `at_most`

Not every goal wants "more" — screen time and calorie limits want you to
stay *under* a number. Every goal has a `direction`:

- `at_least` — met if `today_total >= target * (threshold_pct / 100)`
  (e.g. water: hit 90%+ of 2500ml)
- `at_most` — met if `today_total <= target * (1 + (100 - threshold_pct) / 100)`
  (e.g. screen time: stay within 10% over a 120min cap when threshold_pct=90)

`threshold_pct` always means "how strict is this" — it just bends in the
direction that matches the goal.

## The interesting engineering bit

Two things had to be designed carefully, same spirit as SubTrack's
calendar-arithmetic problem:

1. **What counts as "today"?** Every logged entry computes and stores a
   `local_date` from the goal's own IANA timezone *at log time*
   (`compute_local_date()`), not server time. If a goal's timezone is
   changed later, past entries keep their original `local_date` — history
   is never silently rewritten.

2. **Streaks.** `compute_streaks()` walks backward day-by-day from
   *yesterday* (today is deliberately excluded — an in-progress day should
   never break a streak just because it isn't finished yet) until it hits a
   day that didn't meet the goal, for the current streak. Longest streak is
   found by scanning every day from the goal's first-ever entry forward and
   tracking the longest run of met days.

## Why the tools are `async def`

Every tool here is `async def`, and all SQLite access goes through
`aiosqlite` instead of the stdlib `sqlite3`, for the same reason as the
reference project this was modeled after:

- FastMCP thread-offloads plain `def` tools automatically, so a sync
  version wouldn't literally freeze under light load.
- But `async def` + a **blocking** driver inside it is worse than staying
  sync — FastMCP does *not* thread-offload `async def` tools, so a blocking
  DB call would stall every other concurrent request on the event loop.
- So: go `async def` *and* use a genuinely async driver all the way down.
  This composes cleanly if more awaitable I/O (HTTP calls, etc.) gets added
  later, and doesn't burn a worker thread per in-flight DB call.
- The one exception: `goal_templates.json` is read with plain sync I/O —
  it's a few hundred bytes, read rarely, not worth an `aiofiles` dependency.

## Project structure

```
goaltrack-mcp/
├── server.py          # the whole server — module-level `mcp` object
├── client_test.py     # quick manual smoke-test client
├── pyproject.toml     # project metadata + deps (managed by uv)
├── uv.lock            # locked, reproducible dependency versions
├── .python-version    # pins the Python version uv uses
├── .gitignore
└── README.md
```

`goal_templates.json` and `goaltrack.db` are **not** committed —
`server.py` creates them automatically on first run (`init_templates()` /
`init_db()`). If you want a fixed template list to survive redeploys,
remove `goal_templates.json` from `.gitignore` and commit your edited copy.

## Run it locally (uv)

No manual venv step needed — `uv run` creates and syncs `.venv` from
`uv.lock` automatically the first time you use it.

```
uv run server.py
# Starting MCP server 'GoalTrack' with transport 'http' on http://0.0.0.0:8000/mcp
```

Test it with the included client:

```
uv run python client_test.py
```

If you want to test with a **stdio**-based client (e.g. wiring this into
Claude Desktop for local use), run it via the FastMCP CLI, which overrides
the transport regardless of what's in `__main__`:

```
uv run fastmcp run server.py:mcp --transport stdio
```

### Adding or updating dependencies

Don't hand-edit `pyproject.toml`'s dependency list — let uv manage it:

```
uv add some-package
uv add some-package --upgrade
uv lock --upgrade
```

## Deploy to FastMCP Cloud

1. Push this folder to a GitHub repo — commit `pyproject.toml` **and**
   `uv.lock` (don't commit `.venv/`).
2. Sign in at [fastmcp.cloud](https://fastmcp.cloud) with GitHub and create
   a new project from the repo.
3. Set the **entrypoint** to `server.py:mcp`.
4. Deploy. You'll get a URL like `https://<project>.fastmcp.app/mcp` that
   any MCP client — including Claude, via a custom connector — can call.

### A note on storage

This uses SQLite on local disk, which is not guaranteed to survive a
redeploy on most managed platforms. Once the tool logic feels solid, a good
next exercise is swapping `sqlite3`/`aiosqlite` for a hosted database
(Turso/libSQL, Postgres via `asyncpg`, Supabase) via `os.getenv("DATABASE_URL")`.

## Ideas to extend it

- Add a `notify_if_at_risk` tool that checks all active goals late in the
  day and flags ones that are behind, using `daily_encouragement_prompt`
  (or a new nudge prompt) to draft the message.
- Add a `notes` column on `entries` for context per log (e.g. "gym day").
- Add authentication (FastMCP supports bearer-token auth) and a `user_id`
  column once you're ready to make this a private, multi-user server.
- Add a `weekly_summary` tool that aggregates `history()` output across all
  active goals into one digest.

TDQS

A3.5/5.0

Scored across 10 tools

Disambiguation5/5

Each tool targets a clearly distinct operation: goal CRUD, entry logging/editing/deleting, and progress/history/streak retrieval. Although list_goals includes today's progress inline, it clearly differs in scope from get_progress, which focuses on a single goal. No two tools appear interchangeable.

Naming Consistency4/5

All names use snake_case consistently, and most follow a verb_noun pattern (add_goal, edit_goal, log_entry, get_progress). Minor deviation: history and streak_status are noun-only, but they remain readable and unambiguous.

Tool Count5/5

With 10 tools, the set is well-scoped for a goal-tracking server. Each tool covers a necessary operation in the goal/entry lifecycle, and there is no redundant or filler tool.

Completeness5/5

The surface covers full CRUD for both goals and entries, plus derived analytics like progress, history, and streaks. No obvious gaps remain for the stated daily-goal tracking purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues