Skip to main content
Glama
SafrowLabs

Habit Tracker

by SafrowLabs

Habit Tracker Interactive App

A habit tracker that runs entirely in the browser — no API, no database. Daily, specific-weekday and times-per-week habits; yes/no and count habits (8 glasses of water); skip days that keep the streak; current and best streaks, 30-day completion rate and a year heatmap; undo/redo; JSON export/import and CSV export. State is saved to localStorage when the browser allows it.

A small MCP server (server/) serves the same app to ChatGPT and Claude as an MCP App: one tool, show_habit_tracker, whose UI resource is dist/index.html. The server has no state; the tracker still runs in the chat's iframe and the server never sees any habits.

Built from the Kanban template: store, persistence, host bridge, MCP server, build and test setup are the same. Only the core domain (dates, schedules, stats) and the views are new.

Quick start

npm install
npm run dev          # http://localhost:5173/src/  (unbundled ES modules)
npm run build        # -> dist/index.html  (one self-contained file)
npm start            # MCP server, Streamable HTTP: http://127.0.0.1:3000/mcp
npm run start:stdio  # MCP server over stdio (Claude Desktop / Claude Code)
npm test             # unit + server tests (node:test, no browser; server test needs a build)
npm run test:e2e     # build + browser tests in headless Chrome

dist/index.html is the shippable app: open it straight from disk or embed it in an AI chat interface. It makes no network requests. src/ can't be opened from file:// because browsers block ES modules there — use npm run dev.

Related MCP server: WorkoutGuide MCP

Use in ChatGPT and Claude

Build first (npm run build); the server serves dist/index.html and reads it at startup, so restart it after a rebuild.

ChatGPT and claude.ai (remote) need a public HTTPS URL.

ngrok http 3000                                        # terminal 1 -> https://abc123.ngrok.app
HOST=:: ALLOWED_HOSTS=abc123.ngrok.app npm start       # terminal 2, with the ngrok host allowed
  • ChatGPT: Settings → Apps & Connectors → Advanced → Developer mode on → Create → URL https://abc123.ngrok.app/mcp, no auth.

  • claude.ai: Settings → Connectors → Add custom connector → same URL.

HOST=:: listens on IPv4 and IPv6; ngrok connects to localhost as [::1], so the default 127.0.0.1 gives ERR_NGROK_8012. Free ngrok URLs change on every restart: update ALLOWED_HOSTS each time, or the server answers Invalid Host (403).

For a permanent URL (planned: https://habit-tracker.host.safrow.com/mcp) deploy to any Node host with HOST=0.0.0.0, PORT, and ALLOWED_HOSTS=<your domain>. The server also answers /health and serves the app icon at /favicon.ico.

Env

Default

Purpose

PORT

3000

listen port

HOST

127.0.0.1

bind address; :: or 0.0.0.0 when exposed or deployed

ALLOWED_HOSTS

localhost

Host header allowlist (DNS rebinding guard)

ALLOWED_ORIGINS

localhost

Origin allowlist for browser clients; requests without Origin pass

Claude Desktop / Claude Code (local, no hosting) run the stdio entry point:

{
  "mcpServers": {
    "habit-tracker": { "command": "node", "args": ["/absolute/path/to/habit-tracker/server/stdio.js"] }
  }
}

Claude Code: claude mcp add habit-tracker -- node /absolute/path/to/habit-tracker/server/stdio.js. The terminal can't render the app; the desktop and web apps can.

Inside a host the app connects through src/ui/host.js: it completes the MCP Apps handshake, reports its content height so the host sizes the iframe (Week and Progress cap their height and scroll inside; an open dialog reserves room), follows the host's light/dark theme, and asks the host to download exports (sandboxed iframes often block <a download>). localStorage may be blocked or wiped in the host's sandbox; the app then shows "Not saved" and Export/Import is the backup.

Structure

habit-tracker/
├── manifest.json            app metadata: entry, icon, storage key
├── src/
│   ├── index.html           markup: header, tabs, backup banner, view root, dialogs, live region
│   ├── style.css            tokens (light + dark), habit rows, week grid, ring, heatmap, dialogs
│   ├── app.js               boot: load → store → clock → wire UI → subscribe(render, save) → render
│   ├── habits/              core — pure, no DOM, tested in Node
│   │   ├── constants.js     schema version, storage keys, colours, starter habits, empty state
│   │   ├── dates.js         "YYYY-MM-DD" keys: toKey, addDays, weekday, weekStartOf, range (DST-safe)
│   │   ├── schedule.js      isDue, schedule shapes, week days
│   │   ├── stats.js         streaks, best streak, 30-day rate, heatmap, today progress (memoized)
│   │   ├── actions.js       action creators (make ids, today, timestamps)
│   │   ├── reducer.js       (state, action) → newState, all business rules
│   │   ├── schema.js        migrate + invariant checks for stored / imported data
│   │   ├── store.js         state, dispatch, subscribe, undo/redo history
│   │   └── persistence.js   debounced localStorage save, load, JSON/CSV export, lastExportAt
│   └── ui/                  browser only
│       ├── dom.js           h() / s() element builders, focus helpers
│       ├── tabs.js          renderer: the only code that writes the view DOM; tab switching
│       ├── today-view.js    due list, check buttons, progress ring, empty state + starters
│       ├── week-view.js     habits × days table, visible week
│       ├── detail-view.js   heatmap SVG, stat cards, archive / restore / delete
│       ├── day-cell.js      how one habit-day looks and is announced (shared)
│       ├── format.js        date, streak and rate text
│       ├── controls.js      taps, long-press, toolbar
│       ├── dialogs.js       habit editor, day stepper / skip, backup & settings, confirm
│       ├── keyboard.js      grid navigation, shortcuts
│       ├── celebrate.js     streak milestones (7 / 30 / 100 / 365)
│       ├── clock.js         today, midnight rollover, visibilitychange
│       ├── status.js        aria-live, toasts, "Not saved" badge, backup banner
│       ├── icons.js         inline SVG icons + empty-state illustration
│       ├── theme.js         light / dark switch (remembered; follows OS / host until used)
│       └── host.js          MCP Apps host bridge: handshake, auto-resize, host theme, downloads
├── server/                  MCP server (Node)
│   ├── habits.js            tool show_habit_tracker + resource ui://habit-tracker/app.html
│   ├── html.js              loads dist/index.html and the icon
│   ├── http.js              Streamable HTTP entry (ChatGPT, claude.ai)
│   └── stdio.js             stdio entry (Claude Desktop, Claude Code)
├── assets/icons/habit-tracker.svg
├── scripts/
│   ├── build.js             esbuild bundle + inline CSS/JS/icon → dist/index.html
│   └── serve.js             zero-dependency dev server
├── tests/
│   ├── dates.test.js        DST, leap day, year boundary
│   ├── schedule.test.js
│   ├── stats.test.js        the spec's streak table, weekly streaks, rate, heatmap
│   ├── reducer.test.js
│   ├── schema.test.js
│   ├── store.test.js
│   ├── persistence.test.js  + CSV, lastExportAt
│   ├── server.test.js       real HTTP server: tool, resource, 403 on bad Host / Origin
│   └── e2e/
│       ├── app.e2e.js       headless Chrome against dist/index.html (fake clock)
│       └── host.e2e.js      app inside a minimal MCP Apps host (handshake, resize, theme, download)
├── dist/index.html          build output
├── package.json
└── README.md

How it works

Data flows one way:

event → controller → dispatch(action) → reducer (pure) → store (history) → render + debounced save
                                                                              ↑
                                         clock (midnight / tab visible) → render with new `today`
  • Raw check-ins only. State holds habits, a sparse log (habit → date → count) and skips. Streaks, rates and heatmaps are derived by stats.js on render, memoized on the state reference, and never saved.

  • No clock in the core. Only actions.js and ui/clock.js read the time; today is passed in everywhere else, so every rule is tested with fixed dates.

  • Rules. Done = count ≥ target. Not-due days and skip days neither break nor add to a streak. Today (or the current week) not done yet doesn't break a streak. Times-per-week habits count streaks in weeks. A check-in before the start date moves the start date back. Future check-ins are rejected. Archived habits are frozen at their archive date.

  • Rendering is a full re-render of the active tab per change; focus and inner scroll positions are carried across. User text is only ever set via textContent.

  • Persistence is best effort: blocked or full storage → the app keeps running in memory and shows "Not saved". After 30 days without a JSON export a banner asks for a backup. Corrupt saved data is kept under habit-tracker:v1:corrupt and the app starts empty.

Time zones: days are local calendar dates, so crossing time zones can lose or repeat a day. Schedule and target changes re-judge all past days (no schedule history in v1).

Keyboard

Key

Action

T, W, D

Today, Week, Progress

N

new habit

[ / ]

previous / next week

Space / Enter on a check or cell

check off (count habits: +1 on Today, stepper in Week)

S on a check or cell

skip / unskip that day

Arrow keys in the week grid

move between days and habits; Home / End row ends

↑ / ↓ on Today

move between habits; Alt+↑ / Alt+↓ reorder

Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z

undo, redo

Long-press (500 ms) or right-click a check or cell, or use the ⋯ button, for the day menu (count stepper, skip).

License

MIT © SafrowLabs

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables workout tracking and coaching within Claude conversations, managing exercise configs, logs, streaks, and health metrics via an MCP server with PostgreSQL.
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables users to track daily habits via natural language within ChatGPT, supporting habit creation, check-ins, streak calculation, and a visual React widget for interaction.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Self-hosted fitness tracking MCP server that gives AI assistants access to your nutrition, training, weight, sleep, and accomplishment data via 77 tools and 5 resources. Enables natural-language logging and querying of personal health metrics through Claude or ChatGPT.
    0
    1
    BSD 2-Clause "Simplified"