Skip to main content
Glama
mkadiya20

life-hub-mcp

by mkadiya20
README.md
# Life Hub MCP

A personal remote [MCP](https://modelcontextprotocol.io) server (Cloudflare Worker) that lets
Claude log and read structured life-tracking data — starting with nutrition — into an Obsidian
vault.

You type a freeform meal in Claude ("3 eggs, oatmeal, a chicken breast, rice"); the model parses
it and estimates macros, then calls a tool; the Worker writes one immutable event file to
[Cloudflare R2](https://developers.cloudflare.com/r2/); Obsidian (via the
[Remotely Save](https://github.com/remotely-save/remotely-save) plugin) pulls those files into a
vault folder, where a [Dataview](https://blacksmithgu.github.io/obsidian-dataview/) dashboard
summarizes them.

Nutrition is the first tracker; the design generalizes to future entry types (journal, workout,
etc.) via a shared envelope + a typed body.

## How it works

```
Claude app ──parse+estimate──▶ MCP tool ──▶ Worker ──write──▶ R2 (one file per meal)
                                                                  │
                                              Remotely Save ◀──sync┘
                                                    │
                                                    ▼
                                          Obsidian vault + Dataview dashboard
```

### Storage model

- **One immutable event file per submission.** Never a read-modify-write of a shared daily note.
- Daily/weekly totals are **derived** at read time (and in the dashboard), never stored.
- R2 key mirrors the vault subpath, keyed by logical date so "last 7 days" is a cheap prefix-list:
  ```
  Trackers/Nutrition/entries/2026-06-23/2026-06-23-190712.md
  ```
- Filename stem `{logical_date}-{HHMMSS}` is the idempotency key. A ULID lives inside the file as a
  stable internal `id`.

### Entry format

Each entry is markdown: a YAML frontmatter envelope (common to all types) + a typed body + a
human-readable line.

```yaml
---
id: 01J8XQ2K7E9...
type: nutrition-entry
date: 2026-06-23           # logical date (6am-Toronto rule)
logged_at: 2026-06-23T23:07:12Z
source: claude
meal: dinner
calories: 820             # derived from items
protein_g: 63
confidence: medium
input_text: "3 eggs, oatmeal, a chicken breast, rice"  # raw user text, kept for audit
items:
  - { name: eggs, qty: 3, calories: 210, protein_g: 18 }
  # ...
---

Dinner — 3 eggs, oatmeal, a chicken breast, rice
```

### Logical-date rule

Timezone `America/Toronto`; a "day" runs 6:00am → 6:00am, so a meal logged at 12:30am belongs to
the previous calendar date. Conversion goes through the IANA timezone (DST-aware), never a fixed
offset. An explicit date passed by the user is used verbatim.

## Tools

| Tool | Kind | Purpose |
|------|------|---------|
| `log_meal` | write | Log a meal as one nutrition entry; totals derived from per-unit macros × qty. |
| `log_weight` | write | Log a bodyweight measurement (weight_lbs, optional waist/body-fat/notes). |
| `edit_meal` | write | Correct a nutrition entry by its `key`; recomputes totals. `date` moves the entry. |
| `edit_weight` | write | Correct a bodyweight entry by its `key`. `date` moves the entry. |
| `get_entries` | read | Fetch entries in a date range as compact JSON. Omit `type` for all types; pass it to narrow. |
| `delete_entry` | read | Delete an entry by its `key` + `type` (delete carries no fields, so it stays generic). |
| `get_daily_totals` | read | Per-day nutrition macro rollups for trend questions. |

Writes are typed (one `log_X`/`edit_X` per entry type, so each tool's schema shows exactly that
type's fields); reads are generic over the shared envelope. New entry types are added as an
adapter in `src/registry.ts` — see the [`Adapter`](src/registry.ts) interface (type, folder,
tags, zod input shapes, `toBody`/`applyEdit`/`bodyLine`).

## Stack

- Cloudflare Workers + TypeScript
- Cloudflare R2 (`env.BUCKET`) for storage
- MCP over streamable HTTP, built on the [`agents`](https://www.npmjs.com/package/agents) SDK
- GitHub OAuth via [`@cloudflare/workers-oauth-provider`](https://www.npmjs.com/package/@cloudflare/workers-oauth-provider),
  gated to an allowlist of GitHub logins
- Obsidian + Dataview for the dashboard (frontmatter properties, not inline fields)

## Development

```bash
npm install          # installs deps and the lefthook pre-commit hook
npm run dev          # wrangler dev (local R2 via Miniflare)
npm run check        # lint + format check + type-check
npm run lint:fix     # apply lint fixes
npm run format       # apply formatting
```

Tools can be exercised locally against the `/mcp` endpoint with the
[MCP Inspector](https://github.com/modelcontextprotocol/inspector) or any MCP client.

## Deployment

Bindings (R2 bucket `life-hub`, KV namespace `OAUTH_KV`) are declared in `wrangler.jsonc`.

Secrets (set via `npx wrangler secret put <NAME>`):

- `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` — from a GitHub OAuth App whose callback is
  `https://<worker-url>/callback`
- `COOKIE_ENCRYPTION_KEY` — e.g. `openssl rand -hex 32`

```bash
npx wrangler deploy
```

Then add `https://<worker-url>/mcp` as a custom connector in Claude and authenticate via GitHub.
Only GitHub logins in `ALLOWED_USERNAMES` (in `src/index.ts`) can use the tools.