GoalTrack
# 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
Scored across 10 tools
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.
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.
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.
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.