Habit Tracker
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Habit Trackeropen the habit tracker so I can check off today's habits"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 Chromedist/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 allowedChatGPT: 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 |
|
| listen port |
|
| bind address; |
| localhost |
|
| localhost |
|
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.mdHow 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) andskips. Streaks, rates and heatmaps are derived bystats.json render, memoized on the state reference, and never saved.No clock in the core. Only
actions.jsandui/clock.jsread the time;todayis 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:corruptand 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 |
| Today, Week, Progress |
| new habit |
| previous / next week |
| check off (count habits: +1 on Today, stepper in Week) |
| skip / unskip that day |
Arrow keys in the week grid | move between days and habits; |
| move between habits; |
| 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
This server cannot be deployed
Maintenance
Related MCP Connectors
Track anything by telling your AI: calories, habits, mood, spending and tasks on one dashboard.
Publish a live calculator, tracker, tool, or game from Cursor, Claude, or ChatGPT.
Calorie tracker app for ChatGPT and Claude: read meals, macros, weight and log meals from chat.
WHOOP recovery, strain, sleep and workouts in Claude via official WHOOP OAuth. Free, open source.
Related MCP Servers
- FlicenseAqualityDmaintenancePersonal habit tracking MCP server for a 90-day mission, tracking 6 non-negotiable habits with Supabase backend, exposes tools for logging and viewing progress.4-
- FlicenseNot gradedqualityDmaintenanceEnables workout tracking and coaching within Claude conversations, managing exercise configs, logs, streaks, and health metrics via an MCP server with PostgreSQL.-
- FlicenseNot gradedqualityDmaintenanceEnables users to track daily habits via natural language within ChatGPT, supporting habit creation, check-ins, streak calculation, and a visual React widget for interaction.-
- AlicenseNot gradedqualityBmaintenanceSelf-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.01BSD 2-Clause "Simplified"