cronometer-api-mcp 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ # Exclude noise from the build context
2
+ .git
3
+ .gitignore
4
+ __pycache__
5
+ *.pyc
6
+ .venv
7
+ venv
8
+ .pytest_cache
9
+ .ruff_cache
10
+ dist
11
+ build
12
+ *.egg-info
13
+ flows
14
+ *.md
15
+ !README.md
16
+ Procfile
17
+ opencode.json
18
+ server.json
19
+ skills-lock.json
20
+ session-*.md
21
+ Dockerfile
22
+ .dockerignore
@@ -0,0 +1,2 @@
1
+ CRONOMETER_USERNAME=
2
+ CRONOMETER_PASSWORD=
@@ -0,0 +1,66 @@
1
+ name: Build Docker image
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ release:
7
+ types: [published]
8
+
9
+ concurrency:
10
+ group: docker-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+ packages: write
19
+ # Required for build provenance attestation
20
+ id-token: write
21
+ attestations: write
22
+
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+
26
+ - name: Set up QEMU
27
+ uses: docker/setup-qemu-action@v3
28
+
29
+ - name: Set up Docker Buildx
30
+ uses: docker/setup-buildx-action@v3
31
+
32
+ - name: Log in to GHCR
33
+ uses: docker/login-action@v3
34
+ with:
35
+ registry: ghcr.io
36
+ username: ${{ github.actor }}
37
+ password: ${{ secrets.GITHUB_TOKEN }}
38
+
39
+ - name: Compute image metadata
40
+ id: meta
41
+ uses: docker/metadata-action@v5
42
+ with:
43
+ images: ghcr.io/${{ github.repository }}
44
+ tags: |
45
+ type=semver,pattern={{version}}
46
+ type=semver,pattern={{major}}.{{minor}}
47
+ type=raw,value=latest,enable={{is_default_branch}}
48
+
49
+ - name: Build and push
50
+ id: build
51
+ uses: docker/build-push-action@v5
52
+ with:
53
+ context: .
54
+ platforms: linux/amd64,linux/arm64
55
+ push: true
56
+ tags: ${{ steps.meta.outputs.tags }}
57
+ labels: ${{ steps.meta.outputs.labels }}
58
+ cache-from: type=gha
59
+ cache-to: type=gha,mode=max
60
+
61
+ - name: Generate build provenance attestation
62
+ uses: actions/attest-build-provenance@v1
63
+ with:
64
+ subject-name: ghcr.io/${{ github.repository }}
65
+ subject-digest: ${{ steps.build.outputs.digest }}
66
+ push-to-registry: true
@@ -0,0 +1,51 @@
1
+ # Cronometer MCP image, wrapped with supergateway.
2
+ #
3
+ # Contract (same for every MCP hosted by this gateway):
4
+ # - Exposes MCP streamable-HTTP on 0.0.0.0:$PORT at /mcp
5
+ # - Health endpoint at /healthz
6
+ # - stdio-speaking MCP process is spawned by supergateway as a child;
7
+ # supergateway handles protocol translation, session binding, and
8
+ # server->client SSE notifications.
9
+ #
10
+ # The gateway reverse-proxies HTTP into this container. No stdio, Docker
11
+ # attach, or EOF games are required anywhere.
12
+ #
13
+ # Base image: supercorp/supergateway:uvx (Alpine + Node 20 + uv).
14
+ # We install Python 3.14 via uv (cronometer-api-mcp requires >=3.14).
15
+
16
+ FROM supercorp/supergateway:uvx
17
+
18
+ ENV PYTHONDONTWRITEBYTECODE=1 \
19
+ PYTHONUNBUFFERED=1 \
20
+ UV_LINK_MODE=copy \
21
+ UV_PYTHON_INSTALL_DIR=/opt/uv-python \
22
+ UV_TOOL_DIR=/opt/uv-tools \
23
+ UV_TOOL_BIN_DIR=/usr/local/bin \
24
+ PORT=8080
25
+
26
+ WORKDIR /app
27
+
28
+ # Copy the package source. Uses the .dockerignore alongside this file.
29
+ COPY pyproject.toml README.md ./
30
+ COPY src ./src
31
+
32
+ # Pre-install Python 3.14 into a deterministic location, then install the MCP
33
+ # as a uv tool so its console entry point (`cronometer-api-mcp`) lands in
34
+ # /usr/local/bin on $PATH.
35
+ RUN uv python install 3.14 \
36
+ && uv tool install --python 3.14 . \
37
+ && cronometer-api-mcp --help >/dev/null 2>&1 || true
38
+
39
+ # supergateway wraps the stdio MCP; Nomad (or docker run -p) remaps $PORT.
40
+ # --stateful enables Mcp-Session-Id semantics per the MCP streamable-HTTP spec.
41
+ # --sessionTimeout is unused here because gateway owns reaping via Nomad;
42
+ # we still set a generous value so a forgotten session eventually self-heals.
43
+ ENTRYPOINT ["/bin/sh", "-c", "exec supergateway \
44
+ --stdio 'cronometer-api-mcp' \
45
+ --outputTransport streamableHttp \
46
+ --stateful \
47
+ --streamableHttpPath /mcp \
48
+ --healthEndpoint /healthz \
49
+ --port \"${PORT}\" \
50
+ --sessionTimeout 3600000 \
51
+ --logLevel info"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cronometer-api-mcp
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: MCP server for Cronometer nutrition data using the mobile REST API
5
5
  Author: Randy
6
6
  License-Expression: MIT
@@ -15,6 +15,7 @@ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
15
15
  Requires-Python: >=3.14
16
16
  Requires-Dist: httpx>=0.27.0
17
17
  Requires-Dist: mcp>=1.0.0
18
+ Requires-Dist: python-dotenv>=1.0.0
18
19
  Requires-Dist: uvicorn>=0.30.0
19
20
  Description-Content-Type: text/markdown
20
21
 
@@ -97,7 +98,7 @@ export CRONOMETER_PASSWORD="your-password"
97
98
 
98
99
  | Tool | Description |
99
100
  |------|-------------|
100
- | `get_food_log` | Diary entries for a date with food names, amounts, and meal groups |
101
+ | `get_food_log` | Diary entries for a date with food names, amounts, and meal groups, plus an energy_summary (target/consumed/remaining kcal) |
101
102
  | `get_daily_nutrition` | Daily macro and micronutrient totals |
102
103
  | `get_nutrition_scores` | Category scores (Vitamins, Minerals, etc.) with per-nutrient consumed amounts and confidence levels |
103
104
 
@@ -177,6 +178,18 @@ When deployed remotely with OAuth configured, connect from Claude.ai using:
177
178
 
178
179
  Claude.ai will open a browser tab for authorization. Click **Authorize** to complete the connection.
179
180
 
181
+ ## Development
182
+
183
+ For local development, copy `.env.example` to `.env` and fill in your credentials:
184
+
185
+ ```bash
186
+ cp .env.example .env
187
+ # edit .env
188
+ uv run cronometer-api-mcp
189
+ ```
190
+
191
+ The CLI auto-loads `.env` on startup (dev convenience only). Real environment variables always win over `.env`, so production deployments and MCP client `env` blocks are unaffected.
192
+
180
193
  ## How It Works
181
194
 
182
195
  This server communicates with `mobile.cronometer.com` -- the same REST API used by the Cronometer Android/Flutter app. The API was reverse-engineered through:
@@ -77,7 +77,7 @@ export CRONOMETER_PASSWORD="your-password"
77
77
 
78
78
  | Tool | Description |
79
79
  |------|-------------|
80
- | `get_food_log` | Diary entries for a date with food names, amounts, and meal groups |
80
+ | `get_food_log` | Diary entries for a date with food names, amounts, and meal groups, plus an energy_summary (target/consumed/remaining kcal) |
81
81
  | `get_daily_nutrition` | Daily macro and micronutrient totals |
82
82
  | `get_nutrition_scores` | Category scores (Vitamins, Minerals, etc.) with per-nutrient consumed amounts and confidence levels |
83
83
 
@@ -157,6 +157,18 @@ When deployed remotely with OAuth configured, connect from Claude.ai using:
157
157
 
158
158
  Claude.ai will open a browser tab for authorization. Click **Authorize** to complete the connection.
159
159
 
160
+ ## Development
161
+
162
+ For local development, copy `.env.example` to `.env` and fill in your credentials:
163
+
164
+ ```bash
165
+ cp .env.example .env
166
+ # edit .env
167
+ uv run cronometer-api-mcp
168
+ ```
169
+
170
+ The CLI auto-loads `.env` on startup (dev convenience only). Real environment variables always win over `.env`, so production deployments and MCP client `env` blocks are unaffected.
171
+
160
172
  ## How It Works
161
173
 
162
174
  This server communicates with `mobile.cronometer.com` -- the same REST API used by the Cronometer Android/Flutter app. The API was reverse-engineered through:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cronometer-api-mcp"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "MCP server for Cronometer nutrition data using the mobile REST API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -25,6 +25,7 @@ dependencies = [
25
25
  "mcp>=1.0.0",
26
26
  "httpx>=0.27.0",
27
27
  "uvicorn>=0.30.0",
28
+ "python-dotenv>=1.0.0",
28
29
  ]
29
30
 
30
31
  [project.scripts]
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/rwestergren/cronometer-api-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.1.2",
9
+ "version": "0.1.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "pypi",
13
13
  "identifier": "cronometer-api-mcp",
14
- "version": "0.1.2",
14
+ "version": "0.1.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -371,7 +371,10 @@ class CronometerClient:
371
371
 
372
372
  Args:
373
373
  food_id: Cronometer food ID.
374
- measure_id: Measure/unit ID (from get_food). 0 for gram-based.
374
+ measure_id: Measure/unit ID. Get this from search_food() results
375
+ (measureId field) or get_food() (defaultMeasureId or measures[].id).
376
+ 0 is only valid for user-created custom foods; database-sourced
377
+ foods (CRDB/NCCDB/FDC) require a real measure ID.
375
378
  grams: Weight in grams.
376
379
  translation_id: Translation ID (from search results, usually 0).
377
380
  day: Date to log to. Defaults to today.
@@ -88,6 +88,18 @@ def get_food_log(date: str | None = None) -> str:
88
88
  Returns every food entry logged for the day, including food names,
89
89
  amounts, meal groups, and nutrient data.
90
90
 
91
+ Also returns a top-level energy_summary field with pre-computed
92
+ values most relevant to the user:
93
+
94
+ - total_target_kcal: daily calorie target dynamically adjusted
95
+ for expenditure and weight goal (equivalent to Cronometer's
96
+ "Total Target" in the Energy Summary screen)
97
+ - consumed_kcal: total calories consumed
98
+ - remaining_kcal: calories remaining to stay on target
99
+ (total_target_kcal - consumed_kcal). Always report this
100
+ when summarizing the user's day. Prefer this over manually
101
+ deriving values from the burn breakdown fields.
102
+
91
103
  Args:
92
104
  date: Date as YYYY-MM-DD (defaults to today).
93
105
  """
@@ -95,9 +107,22 @@ def get_food_log(date: str | None = None) -> str:
95
107
  client = _get_client()
96
108
  day = _parse_date(date)
97
109
  data = client.get_diary(day)
110
+
111
+ summary = (data or {}).get("summary") or {}
112
+ target = (summary.get("macros") or {}).get("energy")
113
+ consumed = (summary.get("consumed") or {}).get("total")
114
+ energy_summary: dict | None = None
115
+ if target is not None and consumed is not None:
116
+ energy_summary = {
117
+ "total_target_kcal": target,
118
+ "consumed_kcal": consumed,
119
+ "remaining_kcal": int(round(target - consumed)),
120
+ }
121
+
98
122
  return _ok(
99
123
  {
100
124
  "date": date or str(date_module_today()),
125
+ "energy_summary": energy_summary,
101
126
  "diary": data,
102
127
  }
103
128
  )
@@ -977,6 +1002,15 @@ class OAuthAuthorizationMiddleware:
977
1002
 
978
1003
 
979
1004
  def main():
1005
+ # Load .env for local development. No-op if the file is missing.
1006
+ # override=False keeps real environment variables (Docker, systemd,
1007
+ # MCP client `env` blocks, etc.) authoritative over .env.
1008
+ from dotenv import find_dotenv, load_dotenv
1009
+
1010
+ dotenv_path = find_dotenv(usecwd=True)
1011
+ if dotenv_path and load_dotenv(dotenv_path, override=False):
1012
+ logger.info("Loaded .env from %s", dotenv_path)
1013
+
980
1014
  transport = os.getenv("MCP_TRANSPORT", "stdio")
981
1015
 
982
1016
  if transport == "stdio":
@@ -102,6 +102,7 @@ source = { editable = "." }
102
102
  dependencies = [
103
103
  { name = "httpx" },
104
104
  { name = "mcp" },
105
+ { name = "python-dotenv" },
105
106
  { name = "uvicorn" },
106
107
  ]
107
108
 
@@ -114,6 +115,7 @@ dev = [
114
115
  requires-dist = [
115
116
  { name = "httpx", specifier = ">=0.27.0" },
116
117
  { name = "mcp", specifier = ">=1.0.0" },
118
+ { name = "python-dotenv", specifier = ">=1.0.0" },
117
119
  { name = "uvicorn", specifier = ">=0.30.0" },
118
120
  ]
119
121