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.
- cronometer_api_mcp-0.2.3/.env.example +6 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/Dockerfile +4 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/PKG-INFO +34 -6
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/README.md +31 -2
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/pyproject.toml +9 -4
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/server.json +14 -2
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/src/cronometer_api_mcp/client.py +608 -86
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/src/cronometer_api_mcp/server.py +170 -4
- cronometer_api_mcp-0.2.3/tests/test_client.py +1009 -0
- cronometer_api_mcp-0.2.3/tests/test_server_import.py +142 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/tests/test_timezone.py +178 -3
- cronometer_api_mcp-0.2.3/tests/test_totp.py +107 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/uv.lock +101 -52
- cronometer_api_mcp-0.2.1/.env.example +0 -2
- cronometer_api_mcp-0.2.1/tests/test_client.py +0 -336
- cronometer_api_mcp-0.2.1/tests/test_server_import.py +0 -67
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.dockerignore +0 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.github/dependabot.yml +0 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.github/workflows/ci.yml +0 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.github/workflows/docker.yml +0 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.github/workflows/publish.yml +0 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.gitignore +0 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/.python-version +0 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/LICENSE +0 -0
- {cronometer_api_mcp-0.2.1 → cronometer_api_mcp-0.2.3}/src/cronometer_api_mcp/__init__.py +0 -0
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: cronometer-api-mcp
|
|
3
|
-
Version: 0.2.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
9
|
+
"version": "0.2.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "pypi",
|
|
13
13
|
"identifier": "cronometer-api-mcp",
|
|
14
|
-
"version": "0.2.
|
|
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
|
}
|