Fitaily MCP
by rafafau
README.md
# Fitaily MCP
[](https://github.com/rafafau/fitaily-mcp/actions/workflows/ci.yml)
[](LICENSE)
[](package.json)
Remote [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
Fitaily nutrition, training and progress API as **66 tools** over Streamable HTTP.
> **Companion server.** Fitaily MCP has no database and no business logic of its own: every
> tool calls the Fitaily REST API with the user's OAuth token. You need a running Fitaily
> backend, the same `JWT_SECRET`, and an OAuth client (ChatGPT, Claude, Cursor, OpenCode…).
> It is not a standalone fitness tracker.
---
## What it is
- **Stateless.** One `McpServer` + transport per request; no sessions, no cache, no storage.
- **OAuth 2.1 (PKCE).** The Fitaily backend is the authorization server; this process only
verifies HS256 access tokens (audience, scopes, expiry).
- **Scoped access.** Every tool checks a `*:read` or `*:write` scope before calling the
backend; writes are annotated as `write`/`destructive` so clients can ask first.
- **Thin.** Backend failures are classified as unauthorized (`401`/`403`) or
unreachable/error (`502`) and returned as short tool errors; response bodies are never
forwarded to the model.
- **Structured results.** Every tool declares an `outputSchema` and returns the same JSON in
`structuredContent` and in `content[0].text`, so typed clients can parse results while
text-only clients keep working.
## What it is not
- Not a medical device and not medical advice. It reports what the user logged.
- Not a data store: nothing is persisted, cached or written to disk.
- Not a replacement for the Fitaily REST API — the API remains the source of truth.
## Requirements
| Requirement | Notes |
| --------------- | ------------------------------------------------------------- |
| Node.js 26+ | matches the Docker image and CI |
| pnpm 10+ | `corepack enable` is enough |
| Fitaily backend | must expose `/api/oauth/*` and accept the shared `JWT_SECRET` |
| OAuth client | any MCP client that supports remote HTTP + OAuth discovery |
## Quickstart
```bash
git clone https://github.com/rafafau/fitaily-mcp.git
cd fitaily-mcp
corepack enable
pnpm install
cp .env.example .env # set JWT_SECRET, FITAILY_API_URL and MCP_RESOURCE_URL
pnpm dev
```
```bash
curl -s http://localhost:3091/health
# {"success":true,"service":"fitaily","version":"0.2.0","timestamp":"…"}
```
To exercise `/mcp` locally you need a token minted for this resource. Load the same
`.env` in your shell and create a short-lived development token:
```bash
set -a; . ./.env; set +a
TOKEN=$(node -e "console.log(require('jsonwebtoken').sign(
{ userId: 'dev', scope: 'nutrition:read training:read progress:read', aud: process.env.MCP_RESOURCE_URL },
process.env.JWT_SECRET,
{ expiresIn: '15m', algorithm: 'HS256' }
))")
curl -s http://localhost:3091/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"curl","version":"0"}}}'
```
## Connect a client
The public endpoint is `https://mcp.fitaily.com/mcp`; locally it is
`http://localhost:3091/mcp`. Authentication is discovered automatically through
`/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`.
**Claude Code**
```bash
claude mcp add --transport http fitaily https://mcp.fitaily.com/mcp
```
**Cursor / VS Code / generic JSON config**
```json
{
"mcpServers": {
"fitaily": { "type": "http", "url": "https://mcp.fitaily.com/mcp" }
}
}
```
**OpenCode**
```json
{
"mcp": {
"fitaily": { "type": "remote", "url": "https://mcp.fitaily.com/mcp", "enabled": true }
}
}
```
**Claude Desktop** (no native remote transport — bridge it):
```json
{
"mcpServers": {
"fitaily": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.fitaily.com/mcp"] }
}
}
```
**ChatGPT** — Settings → Connectors → add a custom connector with the endpoint URL, then
complete the OAuth prompt.
During OAuth the client registers itself dynamically, opens the Fitaily consent screen and
receives an access token whose `aud` must equal `MCP_RESOURCE_URL`. A mismatch is rejected
with `403 Token audience mismatch`.
## Configuration
| Variable | Required | Default | Purpose |
| ---------------------- | ---------------- | ----------------------------- | -------------------------------------------------------------------------------------------- |
| `JWT_SECRET` | ✅ | — | Verifies HS256 access tokens. Must equal the backend value. `/mcp` returns `500` without it. |
| `MCP_RESOURCE_URL` | ✅ in production | `https://mcp.fitaily.com/mcp` | Public endpoint URL **and** expected token audience. No trailing slash. |
| `FITAILY_API_URL` | ✅ | `http://localhost:3001/api` | Base URL of the REST API, including `/api`. |
| `OAUTH_ISSUER` | ✅ | `https://fitaily.com` | Authorization server issuer, used in discovery documents and the optional `iss` check. |
| `PORT` | | `3091` | HTTP port. |
| `CORS_ORIGIN` | | `*` | `*` or a comma-separated origin allowlist. |
| `TRUST_PROXY` | | `1` | Reverse-proxy hops for client IP + `req.protocol` (one proxy/tunnel hop by default). Set `false` when directly exposed. |
| `MCP_BODY_LIMIT` | | `1mb` | Max JSON body for `POST /mcp`. |
| `RATE_LIMIT_WINDOW_MS` | | `60000` | Rate-limit window per IP. |
| `RATE_LIMIT_MAX` | | `300` | Requests per window per IP on `/mcp`. |
| `ACCESS_LOG` | | `true` | `false` disables the method/path/status access log. |
## Authentication and scopes
Tokens are HS256 JWTs with `userId`, `client_id`, `scope`, `aud`, `jti` and a 15-minute TTL.
The server pins `HS256`, checks `aud`, and verifies `iss` when the claim is present.
| Scope | Unlocks |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `nutrition:read` | meals, foods, water, recipes, diet plans, pantry, profile |
| `nutrition:write` | log/edit/delete meals and water, change goals, recipe CRUD, diet-plan generate/activate/extend, fridge and shopping list |
| `training:read` | activities, plans, workout templates, exercise catalog, strength stats |
| `training:write` | `log_activity`, workout template create/update/delete |
| `progress:read` | measurements, goals, aggregates, trends, forecasts |
| `progress:write` | add/update measurements and goals |
A `401` response carries `WWW-Authenticate: Bearer resource_metadata="…"` so clients can
find the authorization server. Missing scopes return `403 Missing scope: <scope>`.
**Write confirmation is a client responsibility.** The server marks mutating tools with
`readOnlyHint: false` and deletions with `destructiveHint: true`, but it cannot block a call
the model already decided to make. The bundled [skills](skills/) encode the
“show a summary → wait for yes → call the tool” workflow.
## Tools
Full reference with scopes and read/write/destructive labels: **[docs/tools.md](docs/tools.md)**.
| Group | Tools | Highlights |
| ----------------- | ----- | ---------------------------------------------------------------------- |
| Nutrition | 8 | today's totals, meal history, goals, meal suggestions, meal CRUD |
| Foods | 2 | catalog search, barcode lookup |
| Water | 3 | history, log, delete |
| Recipes | 13 | public catalog, tags, favorites, ratings, CRUD, “fit remaining macros” |
| Diet plans | 5 | list, detail, generate, activate, extend |
| Training | 8 | history, plans, cardio logging, workout template CRUD |
| Exercise catalog | 2 | search, detail |
| Strength stats | 3 | personal records, exercise history, muscle activation |
| Progress | 6 | summary, measurements, goals |
| Stats | 7 | nutrition/activity/measurement aggregates, history, trend, forecast |
| Profile | 3 | profile, coach plan, dashboard summary |
| Fridge & shopping | 6 | fridge items, shopping list |
## Architecture
```
client ──OAuth token──▶ POST /mcp
├─ rate limit (per IP)
├─ verify JWT (HS256, aud, scopes)
├─ parse JSON (max 1 MB)
├─ new McpServer + tools bound to the token
└─ fetch FITAILY_API_URL/... (10–15 s timeout)
```
- One server/transport per request: safe to run multiple replicas, no sticky sessions.
- No database, no queue, no disk writes; rate-limit counters live in process memory.
- The backend body is never returned to the model — only a stable status and a generic
message, so an internal error cannot leak data or HTML.
- Backend quirks worth knowing before changing tools: [docs/backend-contract.md](docs/backend-contract.md).
## Development
```bash
pnpm dev # tsx dev server on :3091 (loads .env when present)
pnpm test # vitest (unit + HTTP integration)
pnpm typecheck # src + tests
pnpm lint # eslint 10 + typescript-eslint
pnpm format # prettier
pnpm build # tsc → dist/
pnpm validate:skills # SKILL.md frontmatter + tool names
```
```
src/
api/ fetch wrapper for the Fitaily API
auth/ JWT verification + scope checks
schemas/ zod input schemas shared by all tools
tools/ one file per domain (66 tools)
config.ts env parsing
server.ts express app, OAuth discovery, /mcp
tests/ vitest + supertest
docs/ tool reference and backend contract
skills/ agent skills that teach clients how to use the tools
```
**Adding or renaming a tool**
1. Add the zod schema in `src/schemas/common.ts`.
2. Register it in `src/tools/<domain>.ts` with a description that says when to call it,
`annotations`, and the matching `requireScope`.
3. Add the row to `docs/tools.md` (the docs test fails otherwise).
4. Add or update a test in `tests/tools.test.ts` and, if it is a new workflow, a skill.
5. Run `pnpm lint && pnpm typecheck && pnpm test`.
## Self-hosting
```bash
docker build -t fitaily-mcp .
docker run --rm -p 3091:3091 --env-file .env fitaily-mcp
```
`docker-compose.yml` next to the backend:
```yaml
services:
mcp:
build: .
env_file: .env
restart: unless-stopped
ports:
- "3091:3091"
# Same private network as the backend; FITAILY_API_URL can then use
# http://backend:3001/api instead of a public hostname.
networks:
- fitaily
networks:
fitaily:
external: true
```
Put a TLS-terminating reverse proxy or tunnel in front of it and keep
`TRUST_PROXY=1`; the container runs as the unprivileged `node` user and exposes a
`HEALTHCHECK` against `/health`.
## Privacy and data handling
- The server stores nothing: no database, no cache, no files, no analytics.
- Access logs contain method, path, status and duration only — never tokens or bodies.
- Health data (meals, weight, workouts) passes through in memory for the duration of a
request. Fitaily's own privacy policy governs the backend data.
- Rate-limit counters are in memory and disappear on restart.
## Security
Please report vulnerabilities privately — see [SECURITY.md](SECURITY.md). Do not open a
public issue for a suspected token leak or auth bypass.
Operational notes:
- `JWT_SECRET` is shared with the backend. Rotating it invalidates every outstanding token.
- Tokens are bearer credentials: never log them, never put them in a URL.
- Keep `MCP_RESOURCE_URL` exact; a trailing slash or wrong scheme breaks the audience check.
- The default rate limit is intentionally generous (300 req/min/IP). Tighten it for public
deployments if your clients make many parallel tool calls.
## Design decisions
- **Generic `outputSchema`.** Every tool advertises `outputSchema: { type: "object" }` rather than 66
per-tool field schemas. Tools return trimmed, purpose-built projections, and the real consumer benefit
is `structuredContent` mirroring `content[0].text`. A per-tool schema would duplicate projections and
break when the backend adds fields.
- **No streaming progress notifications.** Every call is a single backend round-trip (typically
100–800 ms). If a future tool (e.g. AI diet plan generation) needs minutes, progress notifications
can be added then without changing today's clients.
- **ESM modules.** The package is `"type": "module"` and compiled with `module: NodeNext`. This matches
the modern Node/MCP ecosystem; the Docker image builds the TypeScript inside the container and ships
only `dist/`.
## Contributing
Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the local
loop and the tool checklist.
## License
[Apache License 2.0](LICENSE) © 2026 Rafal Sipiatowski. See [NOTICE](NOTICE) for the
attribution notice. The license includes an express patent grant; modified files must carry
prominent change notices when redistributed.
Fitaily is a trademark of its owner. This repository is an independent integration; the
Fitaily backend and mobile apps are not part of it.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues