cronometer-api-mcp 0.2.1__tar.gz → 0.2.3__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.
Files changed (25) hide show
  1. cronometer_api_mcp-0.2.3/.env.example +6 -0
  2. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/Dockerfile +4 -0
  3. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/PKG-INFO +34 -6
  4. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/README.md +31 -2
  5. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/pyproject.toml +9 -4
  6. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/server.json +14 -2
  7. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/src/cronometer_api_mcp/client.py +608 -86
  8. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/src/cronometer_api_mcp/server.py +170 -4
  9. cronometer_api_mcp-0.2.3/tests/test_client.py +1009 -0
  10. cronometer_api_mcp-0.2.3/tests/test_server_import.py +142 -0
  11. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/tests/test_timezone.py +178 -3
  12. cronometer_api_mcp-0.2.3/tests/test_totp.py +107 -0
  13. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/uv.lock +101 -52
  14. cronometer_api_mcp-0.2.1/.env.example +0 -2
  15. cronometer_api_mcp-0.2.1/tests/test_client.py +0 -336
  16. cronometer_api_mcp-0.2.1/tests/test_server_import.py +0 -67
  17. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.dockerignore +0 -0
  18. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.github/dependabot.yml +0 -0
  19. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.github/workflows/ci.yml +0 -0
  20. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.github/workflows/docker.yml +0 -0
  21. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.github/workflows/publish.yml +0 -0
  22. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.gitignore +0 -0
  23. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.python-version +0 -0
  24. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/LICENSE +0 -0
  25. {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/src/cronometer_api_mcp/__init__.py +0 -0
@@ -0,0 +1,6 @@
1
+ CRONOMETER_USERNAME=
2
+ CRONOMETER_PASSWORD=
3
+ # Only for accounts with two-factor authentication: the base32 key shown at 2FA setup
4
+ CRONOMETER_TOTP_SECRET=
5
+ # Optional IANA timezone override, for example America/Los_Angeles
6
+ CRONOMETER_ACCOUNT_TZ=
@@ -51,6 +51,10 @@ RUN uv python install 3.14 \
51
51
  # in supergateway's own node_modules to a release that covers the versions the
52
52
  # Python `mcp` package negotiates.
53
53
  #
54
+ # Python mcp 2.x still handshakes at 2025-11-25, which this Node SDK range
55
+ # covers; its 2026-07-28 revision is reachable only via `server/discover`.
56
+ # Re-check this pin only if a release adds a *handshake* version.
57
+ #
54
58
  # NOTE: this is a per-image mitigation, not the durable fix. supergateway
55
59
  # proxies `initialize` verbatim but then validates the negotiated version
56
60
  # against its own (possibly older) SDK, so the skew recurs whenever the wrapped
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: cronometer-api-mcp
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: MCP server for Cronometer nutrition data using the mobile REST API
5
5
  Author: Randy
6
6
  License-Expression: MIT
@@ -11,10 +11,9 @@ Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.14
14
- Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
15
14
  Requires-Python: >=3.14
16
15
  Requires-Dist: httpx[socks]>=0.27.0
17
- Requires-Dist: mcp<2,>=1.29.0
16
+ Requires-Dist: mcp<3,>=2.0.0
18
17
  Requires-Dist: python-dotenv>=1.0.0
19
18
  Requires-Dist: tzdata>=2024.1
20
19
  Description-Content-Type: text/markdown
@@ -60,6 +59,20 @@ export CRONOMETER_USERNAME="your@email.com"
60
59
  export CRONOMETER_PASSWORD="your-password"
61
60
  ```
62
61
 
62
+ #### Optional: two-factor authentication
63
+
64
+ If the account has two-factor authentication enabled, `/api/v2/login`
65
+ answers `TOTP_CODE_REQUIRED` unless the request carries the current 6-digit
66
+ code. Give the server the base32 key that Cronometer showed when 2FA was set
67
+ up (the same key you scanned into your authenticator app) and it derives the
68
+ code itself at every login (RFC 6238, SHA-1, 30 s period):
69
+
70
+ ```bash
71
+ export CRONOMETER_TOTP_SECRET="ABCD EFGH IJKL MNOP QRST UVWX YZ23 4567"
72
+ ```
73
+
74
+ Spaces and lowercase are fine. Leave it unset for accounts without 2FA.
75
+
63
76
  #### Optional: override the account timezone
64
77
 
65
78
  Diary entries are stamped in your Cronometer account's timezone, which the
@@ -89,7 +102,9 @@ cached session, so it also overrides a stale cached timezone.
89
102
  "command": ["uvx", "cronometer-api-mcp"],
90
103
  "environment": {
91
104
  "CRONOMETER_USERNAME": "{env:CRONOMETER_USERNAME}",
92
- "CRONOMETER_PASSWORD": "{env:CRONOMETER_PASSWORD}"
105
+ "CRONOMETER_PASSWORD": "{env:CRONOMETER_PASSWORD}",
106
+ "CRONOMETER_TOTP_SECRET": "{env:CRONOMETER_TOTP_SECRET}",
107
+ "CRONOMETER_ACCOUNT_TZ": "{env:CRONOMETER_ACCOUNT_TZ}"
93
108
  },
94
109
  "enabled": true
95
110
  }
@@ -107,7 +122,9 @@ cached session, so it also overrides a stale cached timezone.
107
122
  "args": ["cronometer-api-mcp"],
108
123
  "env": {
109
124
  "CRONOMETER_USERNAME": "your@email.com",
110
- "CRONOMETER_PASSWORD": "your-password"
125
+ "CRONOMETER_PASSWORD": "your-password",
126
+ "CRONOMETER_TOTP_SECRET": "your-base32-key",
127
+ "CRONOMETER_ACCOUNT_TZ": "America/Los_Angeles"
111
128
  }
112
129
  }
113
130
  }
@@ -138,6 +155,8 @@ cached session, so it also overrides a stale cached timezone.
138
155
  | `add_food_entry` | Log a food serving to the diary |
139
156
  | `remove_food_entry` | Remove one or more diary entries |
140
157
  | `add_custom_food` | Create a custom food with specified nutrition |
158
+ | `add_recipe` | Create a recipe from existing foods referenced by ID and gram weight |
159
+ | `import_recipe` | Create a recipe from a free-text ingredient list; Cronometer matches each line to a database food and converts the amount to grams |
141
160
  | `copy_day` | Copy all entries from the previous day |
142
161
  | `mark_day_complete` | Mark a diary day as complete or incomplete |
143
162
 
@@ -186,6 +205,8 @@ The API uses two protocols:
186
205
  - **v2 (`POST /api/v2/*`)** -- JSON-body auth, used for most operations (food search, diary read/write, nutrition, fasting, macros, biometrics)
187
206
  - **v3 (`DELETE /api/v3/user/{id}/*`)** -- Header-based auth (`x-crono-session`), used for diary entry deletion
188
207
 
208
+ Recipe import is the one asynchronous operation: `import_recipe` returns a job id, and `poll_async_result` is polled until the server reports 100% progress and attaches the parsed ingredients.
209
+
189
210
  ## Python API
190
211
 
191
212
  You can use the client directly:
@@ -212,6 +233,13 @@ client.add_serving(
212
233
  # Get today's diary
213
234
  diary = client.get_diary()
214
235
 
236
+ # Import a recipe from a free-text ingredient list
237
+ recipe = client.import_recipe("one hot dog\nketchup\nbun")
238
+ print(recipe["food_id"], recipe["ingredients"])
239
+
240
+ # Parse without saving, to review the matches first
241
+ preview = client.import_recipe("2 tbsp olive oil\n200g chicken", save=False)
242
+
215
243
  # Get nutrition scores
216
244
  scores = client.get_nutrition_scores()
217
245
  ```
@@ -39,6 +39,20 @@ export CRONOMETER_USERNAME="your@email.com"
39
39
  export CRONOMETER_PASSWORD="your-password"
40
40
  ```
41
41
 
42
+ #### Optional: two-factor authentication
43
+
44
+ If the account has two-factor authentication enabled, `/api/v2/login`
45
+ answers `TOTP_CODE_REQUIRED` unless the request carries the current 6-digit
46
+ code. Give the server the base32 key that Cronometer showed when 2FA was set
47
+ up (the same key you scanned into your authenticator app) and it derives the
48
+ code itself at every login (RFC 6238, SHA-1, 30 s period):
49
+
50
+ ```bash
51
+ export CRONOMETER_TOTP_SECRET="ABCD EFGH IJKL MNOP QRST UVWX YZ23 4567"
52
+ ```
53
+
54
+ Spaces and lowercase are fine. Leave it unset for accounts without 2FA.
55
+
42
56
  #### Optional: override the account timezone
43
57
 
44
58
  Diary entries are stamped in your Cronometer account's timezone, which the
@@ -68,7 +82,9 @@ cached session, so it also overrides a stale cached timezone.
68
82
  "command": ["uvx", "cronometer-api-mcp"],
69
83
  "environment": {
70
84
  "CRONOMETER_USERNAME": "{env:CRONOMETER_USERNAME}",
71
- "CRONOMETER_PASSWORD": "{env:CRONOMETER_PASSWORD}"
85
+ "CRONOMETER_PASSWORD": "{env:CRONOMETER_PASSWORD}",
86
+ "CRONOMETER_TOTP_SECRET": "{env:CRONOMETER_TOTP_SECRET}",
87
+ "CRONOMETER_ACCOUNT_TZ": "{env:CRONOMETER_ACCOUNT_TZ}"
72
88
  },
73
89
  "enabled": true
74
90
  }
@@ -86,7 +102,9 @@ cached session, so it also overrides a stale cached timezone.
86
102
  "args": ["cronometer-api-mcp"],
87
103
  "env": {
88
104
  "CRONOMETER_USERNAME": "your@email.com",
89
- "CRONOMETER_PASSWORD": "your-password"
105
+ "CRONOMETER_PASSWORD": "your-password",
106
+ "CRONOMETER_TOTP_SECRET": "your-base32-key",
107
+ "CRONOMETER_ACCOUNT_TZ": "America/Los_Angeles"
90
108
  }
91
109
  }
92
110
  }
@@ -117,6 +135,8 @@ cached session, so it also overrides a stale cached timezone.
117
135
  | `add_food_entry` | Log a food serving to the diary |
118
136
  | `remove_food_entry` | Remove one or more diary entries |
119
137
  | `add_custom_food` | Create a custom food with specified nutrition |
138
+ | `add_recipe` | Create a recipe from existing foods referenced by ID and gram weight |
139
+ | `import_recipe` | Create a recipe from a free-text ingredient list; Cronometer matches each line to a database food and converts the amount to grams |
120
140
  | `copy_day` | Copy all entries from the previous day |
121
141
  | `mark_day_complete` | Mark a diary day as complete or incomplete |
122
142
 
@@ -165,6 +185,8 @@ The API uses two protocols:
165
185
  - **v2 (`POST /api/v2/*`)** -- JSON-body auth, used for most operations (food search, diary read/write, nutrition, fasting, macros, biometrics)
166
186
  - **v3 (`DELETE /api/v3/user/{id}/*`)** -- Header-based auth (`x-crono-session`), used for diary entry deletion
167
187
 
188
+ Recipe import is the one asynchronous operation: `import_recipe` returns a job id, and `poll_async_result` is polled until the server reports 100% progress and attaches the parsed ingredients.
189
+
168
190
  ## Python API
169
191
 
170
192
  You can use the client directly:
@@ -191,6 +213,13 @@ client.add_serving(
191
213
  # Get today's diary
192
214
  diary = client.get_diary()
193
215
 
216
+ # Import a recipe from a free-text ingredient list
217
+ recipe = client.import_recipe("one hot dog\nketchup\nbun")
218
+ print(recipe["food_id"], recipe["ingredients"])
219
+
220
+ # Parse without saving, to review the matches first
221
+ preview = client.import_recipe("2 tbsp olive oil\n200g chicken", save=False)
222
+
194
223
  # Get nutrition scores
195
224
  scores = client.get_nutrition_scores()
196
225
  ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cronometer-api-mcp"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "MCP server for Cronometer nutrition data using the mobile REST API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -19,11 +19,9 @@ classifiers = [
19
19
  "License :: OSI Approved :: MIT License",
20
20
  "Programming Language :: Python :: 3",
21
21
  "Programming Language :: Python :: 3.14",
22
- "Topic :: Scientific/Engineering :: Medical Science Apps.",
23
22
  ]
24
23
  dependencies = [
25
- # <2 until the MCPServer migration: 2.x moved mcp.server.fastmcp
26
- "mcp>=1.29.0,<2",
24
+ "mcp>=2.0.0,<3",
27
25
  "httpx[socks]>=0.27.0",
28
26
  "python-dotenv>=1.0.0",
29
27
  "tzdata>=2024.1",
@@ -40,3 +38,10 @@ dev = [
40
38
 
41
39
  [tool.hatch.build.targets.wheel]
42
40
  packages = ["src/cronometer_api_mcp"]
41
+
42
+ [tool.ruff.lint.per-file-ignores]
43
+ # Tools have to hand every failure back as a structured error string; an
44
+ # exception escaping into the stdio stream kills the session.
45
+ "src/cronometer_api_mcp/server.py" = ["BLE001"]
46
+ # Diary enrichment is best-effort and degrades to an unenriched diary.
47
+ "src/cronometer_api_mcp/client.py" = ["BLE001"]
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/rwestergren/cronometer-api-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.2.1",
9
+ "version": "0.2.3",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "pypi",
13
13
  "identifier": "cronometer-api-mcp",
14
- "version": "0.2.1",
14
+ "version": "0.2.3",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -27,6 +27,18 @@
27
27
  "description": "Your Cronometer account password",
28
28
  "isRequired": true,
29
29
  "isSecret": true
30
+ },
31
+ {
32
+ "name": "CRONOMETER_TOTP_SECRET",
33
+ "description": "Base32 key for accounts with two-factor authentication",
34
+ "isRequired": false,
35
+ "isSecret": true
36
+ },
37
+ {
38
+ "name": "CRONOMETER_ACCOUNT_TZ",
39
+ "description": "Optional IANA timezone override, for example America/Los_Angeles",
40
+ "isRequired": false,
41
+ "isSecret": false
30
42
  }
31
43
  ]
32
44
  }