cronometer-mcp 2.2.0__tar.gz → 2.4.0__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 (23) hide show
  1. cronometer_mcp-2.4.0/.github/workflows/publish.yml +79 -0
  2. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/.gitignore +3 -0
  3. cronometer_mcp-2.4.0/AGENTS.md +15 -0
  4. cronometer_mcp-2.4.0/CLAUDE.md +1 -0
  5. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/PKG-INFO +64 -2
  6. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/README.md +62 -0
  7. cronometer_mcp-2.4.0/SPEC-streamable-http.md +141 -0
  8. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/cronometer_mcp/__init__.py +1 -1
  9. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/cronometer_mcp/client.py +241 -295
  10. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/cronometer_mcp/server.py +121 -30
  11. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/pyproject.toml +3 -4
  12. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/release.sh +2 -2
  13. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/server.json +2 -2
  14. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/tests/test_client.py +247 -69
  15. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/tests/test_diary_groups.py +15 -0
  16. cronometer_mcp-2.4.0/tests/test_server.py +193 -0
  17. cronometer_mcp-2.2.0/.github/workflows/publish.yml +0 -28
  18. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/LICENSE +0 -0
  19. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/RESEARCH-copy-repeat-gwt-rpc.md +0 -0
  20. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/SPEC-diary-groups.md +0 -0
  21. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/cronometer_mcp/markdown.py +0 -0
  22. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/tests/__init__.py +0 -0
  23. {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/tests/test_markdown.py +0 -0
@@ -0,0 +1,79 @@
1
+ name: Publish to PyPI and MCP Registry
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ # Manual runs skip PyPI and publish an already-released tag's server.json to
7
+ # the MCP Registry, e.g. to catch up or retry after a registry outage.
8
+ workflow_dispatch:
9
+ inputs:
10
+ tag:
11
+ description: "Release tag to publish to the MCP Registry, e.g. v2.3.0"
12
+ required: true
13
+
14
+ permissions:
15
+ id-token: write
16
+
17
+ jobs:
18
+ publish:
19
+ if: github.event_name == 'release'
20
+ runs-on: ubuntu-latest
21
+ environment: pypi
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.12"
28
+
29
+ - name: Install build tools
30
+ run: pip install build
31
+
32
+ - name: Build package
33
+ run: python -m build
34
+
35
+ - name: Publish to PyPI
36
+ uses: pypa/gh-action-pypi-publish@release/v1
37
+
38
+ mcp-registry:
39
+ needs: publish
40
+ # After a release's PyPI upload, or on its own from a manual run.
41
+ if: ${{ !cancelled() && (needs.publish.result == 'success' || needs.publish.result == 'skipped') }}
42
+ runs-on: ubuntu-latest
43
+ permissions:
44
+ contents: read
45
+ id-token: write
46
+ env:
47
+ MCP_PUBLISHER_VERSION: "1.8.1"
48
+ TAG: ${{ inputs.tag || github.ref_name }}
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+ with:
52
+ ref: ${{ inputs.tag || github.ref }}
53
+
54
+ - name: Check server.json matches the tag
55
+ run: |
56
+ jq -e --arg v "${TAG#v}" '.version == $v and (.packages | length) > 0 and all(.packages[]; .version == $v)' server.json \
57
+ || { echo "server.json version does not match $TAG"; exit 1; }
58
+
59
+ - name: Install mcp-publisher
60
+ run: |
61
+ base="https://github.com/modelcontextprotocol/registry/releases/download/v${MCP_PUBLISHER_VERSION}"
62
+ curl -sSfLO "$base/mcp-publisher_linux_amd64.tar.gz"
63
+ curl -sSfLO "$base/registry_${MCP_PUBLISHER_VERSION}_checksums.txt"
64
+ grep ' mcp-publisher_linux_amd64.tar.gz$' "registry_${MCP_PUBLISHER_VERSION}_checksums.txt" | sha256sum -c -
65
+ tar xzf mcp-publisher_linux_amd64.tar.gz mcp-publisher
66
+
67
+ - name: Authenticate to MCP Registry
68
+ run: ./mcp-publisher login github-oidc
69
+
70
+ - name: Publish server.json
71
+ # The registry checks the version exists on PyPI, which can lag the
72
+ # upload by a minute or so.
73
+ run: |
74
+ for attempt in 1 2 3 4 5; do
75
+ ./mcp-publisher publish && exit 0
76
+ echo "Publish attempt $attempt failed; retrying in 30s"
77
+ sleep 30
78
+ done
79
+ exit 1
@@ -24,5 +24,8 @@ venv/
24
24
  # Data (generated by sync)
25
25
  data/
26
26
 
27
+ # Logs written by local MCP tooling
28
+ logs/
29
+
27
30
  # GWT capture notes (contain session secrets)
28
31
  gwt-captures.md
@@ -0,0 +1,15 @@
1
+ # AGENTS.md
2
+
3
+ Instructions for coding agents working in this repo.
4
+
5
+ ## Releasing
6
+
7
+ - Always release with `./release.sh <version>`. Never bump `pyproject.toml`,
8
+ `cronometer_mcp/__init__.py` or `server.json` by hand, and never hand-roll the bump commit, tag
9
+ or push. The script checks for a clean tree and an unused tag, bumps all three files, commits,
10
+ tags `v<version>` and pushes `main` with tags.
11
+ - Before running it: the working tree is clean and `python -m pytest` passes.
12
+ - The tag alone does not publish. Create a GitHub release from the tag; that triggers
13
+ `.github/workflows/publish.yml`, which publishes to PyPI and then publishes `server.json` to the
14
+ MCP Registry (GitHub OIDC, no secrets). Never publish to the registry by hand; if that job
15
+ fails, re-run it with a manual `workflow_dispatch` of the same workflow, which skips PyPI.
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cronometer-mcp
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: MCP server for Cronometer nutrition data — pull food logs, macros, and micronutrients via the Model Context Protocol
5
5
  Project-URL: Homepage, https://github.com/cphoskins/cronometer-mcp
6
6
  Project-URL: Repository, https://github.com/cphoskins/cronometer-mcp
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
20
20
  Requires-Python: >=3.11
21
- Requires-Dist: mcp<2.0.0,>=1.0.0
21
+ Requires-Dist: mcp<3.0.0,>=2.3.0
22
22
  Requires-Dist: requests>=2.28.0
23
23
  Description-Content-Type: text/markdown
24
24
 
@@ -111,6 +111,68 @@ If you installed from source with `pip install -e .`, you can also use the full
111
111
  }
112
112
  ```
113
113
 
114
+ ### Read-only mode
115
+
116
+ Set `CRONOMETER_READ_ONLY=1` (also accepts `true` or `yes`) in the server's `env` to expose only the
117
+ read tools. The 13 tools that change your Cronometer account are not registered at all, so the
118
+ client never sees them: adding or removing food entries, macro targets, schedules and templates,
119
+ fasts, biometrics, copy day, mark day complete, and repeat items. `sync_cronometer` stays available;
120
+ it only writes files on your machine.
121
+
122
+ This limits what the server offers, not what your credentials can do. Cronometer has no read-only
123
+ login, so the password in `env` still has full access to the account.
124
+
125
+ ### Remote access (streamable HTTP)
126
+
127
+ By default the server talks to its client over stdio. To run it on another machine or in a
128
+ container, set `CRONOMETER_TRANSPORT=streamable-http` and it serves the MCP endpoint at
129
+ `http://<host>:<port>/mcp` instead.
130
+
131
+ | Variable | Default | Meaning |
132
+ |---|---|---|
133
+ | `CRONOMETER_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
134
+ | `CRONOMETER_HOST` | `127.0.0.1` | Interface to bind. Use `0.0.0.0` inside a container. |
135
+ | `CRONOMETER_PORT` | `8000` | TCP port, 1 to 65535 |
136
+ | `CRONOMETER_ALLOWED_HOSTS` | unset | Comma-separated `Host` header values clients may use, as `host:port` or `host:*`. Set it whenever the bind host is not loopback. |
137
+
138
+ In Docker, against the published package, reachable as `your-server` on your network:
139
+
140
+ ```bash
141
+ docker run --rm -p 8000:8000 \
142
+ -e CRONOMETER_USERNAME=your@email.com \
143
+ -e CRONOMETER_PASSWORD=your-password \
144
+ -e CRONOMETER_TRANSPORT=streamable-http \
145
+ -e CRONOMETER_HOST=0.0.0.0 \
146
+ -e CRONOMETER_ALLOWED_HOSTS=your-server:8000 \
147
+ python:3.12-slim sh -c "pip install -q cronometer-mcp && cronometer-mcp"
148
+ ```
149
+
150
+ `CRONOMETER_ALLOWED_HOSTS` matters: the MCP SDK checks `Host` and `Origin` headers on its own only
151
+ when bound to loopback. On any other bind it accepts every `Host`, which lets a web page you happen
152
+ to open on the same network reach the server through DNS rebinding. With the allowlist set,
153
+ requests whose `Host` or `Origin` is not on it get `421` or `403`. Without it the server logs a
154
+ warning at startup and runs anyway.
155
+
156
+ Then point the client at the URL. Claude Code (`.mcp.json`):
157
+
158
+ ```json
159
+ {
160
+ "mcpServers": {
161
+ "cronometer": {
162
+ "type": "http",
163
+ "url": "http://your-server:8000/mcp"
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ The endpoint has no authentication. Anyone who can reach the port can read and change the
170
+ Cronometer account whose credentials the server holds. Keep it bound to `127.0.0.1`, or put it on
171
+ a network only you can reach (a VPN or Tailscale keeps strangers out; it does not stop the DNS
172
+ rebinding case above, so set the allowlist as well) or behind a reverse proxy that authenticates.
173
+ Adding `CRONOMETER_READ_ONLY=1` narrows what a remote client can do, with the same caveat as above:
174
+ the credentials themselves keep full access.
175
+
114
176
  ## Available Tools
115
177
 
116
178
  ### Food Log & Nutrition
@@ -87,6 +87,68 @@ If you installed from source with `pip install -e .`, you can also use the full
87
87
  }
88
88
  ```
89
89
 
90
+ ### Read-only mode
91
+
92
+ Set `CRONOMETER_READ_ONLY=1` (also accepts `true` or `yes`) in the server's `env` to expose only the
93
+ read tools. The 13 tools that change your Cronometer account are not registered at all, so the
94
+ client never sees them: adding or removing food entries, macro targets, schedules and templates,
95
+ fasts, biometrics, copy day, mark day complete, and repeat items. `sync_cronometer` stays available;
96
+ it only writes files on your machine.
97
+
98
+ This limits what the server offers, not what your credentials can do. Cronometer has no read-only
99
+ login, so the password in `env` still has full access to the account.
100
+
101
+ ### Remote access (streamable HTTP)
102
+
103
+ By default the server talks to its client over stdio. To run it on another machine or in a
104
+ container, set `CRONOMETER_TRANSPORT=streamable-http` and it serves the MCP endpoint at
105
+ `http://<host>:<port>/mcp` instead.
106
+
107
+ | Variable | Default | Meaning |
108
+ |---|---|---|
109
+ | `CRONOMETER_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
110
+ | `CRONOMETER_HOST` | `127.0.0.1` | Interface to bind. Use `0.0.0.0` inside a container. |
111
+ | `CRONOMETER_PORT` | `8000` | TCP port, 1 to 65535 |
112
+ | `CRONOMETER_ALLOWED_HOSTS` | unset | Comma-separated `Host` header values clients may use, as `host:port` or `host:*`. Set it whenever the bind host is not loopback. |
113
+
114
+ In Docker, against the published package, reachable as `your-server` on your network:
115
+
116
+ ```bash
117
+ docker run --rm -p 8000:8000 \
118
+ -e CRONOMETER_USERNAME=your@email.com \
119
+ -e CRONOMETER_PASSWORD=your-password \
120
+ -e CRONOMETER_TRANSPORT=streamable-http \
121
+ -e CRONOMETER_HOST=0.0.0.0 \
122
+ -e CRONOMETER_ALLOWED_HOSTS=your-server:8000 \
123
+ python:3.12-slim sh -c "pip install -q cronometer-mcp && cronometer-mcp"
124
+ ```
125
+
126
+ `CRONOMETER_ALLOWED_HOSTS` matters: the MCP SDK checks `Host` and `Origin` headers on its own only
127
+ when bound to loopback. On any other bind it accepts every `Host`, which lets a web page you happen
128
+ to open on the same network reach the server through DNS rebinding. With the allowlist set,
129
+ requests whose `Host` or `Origin` is not on it get `421` or `403`. Without it the server logs a
130
+ warning at startup and runs anyway.
131
+
132
+ Then point the client at the URL. Claude Code (`.mcp.json`):
133
+
134
+ ```json
135
+ {
136
+ "mcpServers": {
137
+ "cronometer": {
138
+ "type": "http",
139
+ "url": "http://your-server:8000/mcp"
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ The endpoint has no authentication. Anyone who can reach the port can read and change the
146
+ Cronometer account whose credentials the server holds. Keep it bound to `127.0.0.1`, or put it on
147
+ a network only you can reach (a VPN or Tailscale keeps strangers out; it does not stop the DNS
148
+ rebinding case above, so set the allowlist as well) or behind a reverse proxy that authenticates.
149
+ Adding `CRONOMETER_READ_ONLY=1` narrows what a remote client can do, with the same caveat as above:
150
+ the credentials themselves keep full access.
151
+
90
152
  ## Available Tools
91
153
 
92
154
  ### Food Log & Nutrition
@@ -0,0 +1,141 @@
1
+ # Spec — Streamable HTTP transport
2
+
3
+ **Origin:** feature request from `phazethru`,
4
+ [discussion #12](https://github.com/cphoskins/cronometer-mcp/discussions/12), 2026-10-24. They run
5
+ the server in a remote Docker container behind `mcpo`, and `mcpo` no longer works with the `mcp`
6
+ package that 2.3.0 requires. A native HTTP endpoint removes `mcpo` from their stack.
7
+
8
+ **Status:** shipped in 2.4.0 (2026-10-24). 201 tests pass; live `list_tools` over HTTP returns
9
+ 27 tools, 14 read-only; hostile `Host` rejected with 421 when the allowlist is set. Codex review:
10
+ round 1 found the DNS-rebinding gap (fixed with `CRONOMETER_ALLOWED_HOSTS`), round 2 SHIP.
11
+
12
+ ---
13
+
14
+ ## Established facts (verified against mcp 2.3.0, do not re-derive)
15
+
16
+ - `MCPServer.run(transport="streamable-http", host=..., port=...)` is supported natively. Defaults
17
+ are `host="127.0.0.1"`, `port=8000`, path `/mcp`.
18
+ - `uvicorn` and `starlette` are hard dependencies of `mcp` already. No new packages.
19
+ - DNS-rebinding protection (Host/Origin header checks) is auto-enabled only when the bind host is
20
+ `127.0.0.1`, `localhost` or `::1`. Any other host (for example `0.0.0.0` in a container) gets no
21
+ header checks, so a Docker publish works without further configuration.
22
+ - The server has no authentication of its own. Whoever can reach the port can act as the
23
+ Cronometer account whose credentials are in the environment.
24
+
25
+ ## Design decisions
26
+
27
+ - **Variable names follow the existing `CRONOMETER_*` convention**, not the requester's
28
+ `STREAMABLE_HTTP=true`. A transport name (`CRONOMETER_TRANSPORT`) covers a future `sse` option
29
+ without adding another boolean, and reads consistently next to `CRONOMETER_READ_ONLY` and
30
+ `CRONOMETER_DATA_DIR`.
31
+ - **Default stays `stdio`.** Every existing client config keeps working unchanged.
32
+ - **Default host stays `127.0.0.1`.** Binding wider is an explicit choice the operator makes and
33
+ the README names the consequence.
34
+
35
+ ---
36
+
37
+ ## 1. Transport selection in `main()`
38
+
39
+ **DO**
40
+ - Read `CRONOMETER_TRANSPORT`. Accept `stdio` (default when unset or empty) and
41
+ `streamable-http`. Compare case-insensitively after stripping whitespace.
42
+ - Read `CRONOMETER_HOST` (default `127.0.0.1`) and `CRONOMETER_PORT` (default `8000`, must be
43
+ an integer from 1 to 65535; Linux silently wraps 65536 to port 0).
44
+ - Read `CRONOMETER_ALLOWED_HOSTS` (comma-separated `Host` header values, `host:port` or
45
+ `host:*`). When set, pass a `TransportSecuritySettings` with DNS-rebinding protection on, those
46
+ hosts, and `http://`/`https://` origins derived from them. When unset on a non-loopback bind, log
47
+ a warning that any `Host` is accepted. (Codex finding 1, 2026-10-24: the SDK auto-enables header
48
+ checks only for loopback binds, so a `0.0.0.0` container is open to DNS rebinding otherwise.)
49
+ - For `streamable-http`, call `mcp.run(transport="streamable-http", host=host, port=port,
50
+ transport_security=...)` (the last only when the allowlist is set).
51
+ - Bracket IPv6 hosts in the logged URL (`http://[::1]:8000/mcp`).
52
+ - For `stdio`, call `mcp.run(transport="stdio")` exactly as today.
53
+ - Log the transport, host and port at startup for `streamable-http` so a container log shows
54
+ where the server is listening.
55
+ - On an unknown transport or a non-integer port, log one clear line naming the variable and the
56
+ bad value, then exit non-zero.
57
+
58
+ **DON'T**
59
+ - Don't add `sse`. It is deprecated in the MCP spec and nobody asked for it.
60
+ - Don't expose further `run_streamable_http_async` options (stateless, json_response, path,
61
+ session limits) as environment variables. Add them when someone needs them.
62
+ - Don't add authentication, TLS or a reverse proxy. Document the exposure instead (§3).
63
+ - Don't touch tool registration, the client lock or read-only mode. Transport is orthogonal.
64
+ - Don't add new dependencies to `pyproject.toml`.
65
+
66
+ **DONE WHEN**
67
+ - `main()` with no transport variables set calls `mcp.run(transport="stdio")`.
68
+ - `CRONOMETER_TRANSPORT=streamable-http` with no host or port set calls
69
+ `mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)`.
70
+ - `CRONOMETER_HOST=0.0.0.0 CRONOMETER_PORT=9000` are passed through as `"0.0.0.0"` and `9000`
71
+ (an `int`).
72
+ - `CRONOMETER_TRANSPORT=carrier-pigeon`, `sse`, and `CRONOMETER_PORT` of `abc`, `-1`, `0`,
73
+ `65536` each exit non-zero without calling `mcp.run`; `1` and `65535` are accepted.
74
+ - `CRONOMETER_ALLOWED_HOSTS=nas.local:8000` on a `0.0.0.0` bind makes a request with
75
+ `Host: evil.example:8000` get `421`, while `Host: nas.local:8000` initializes normally (live,
76
+ not mocked).
77
+ - All of the above are covered by tests in `tests/test_server.py` that patch `server.mcp.run`,
78
+ and `python -m pytest` passes.
79
+
80
+ ## 2. Live smoke test
81
+
82
+ **DO**
83
+ - Start the server with `CRONOMETER_TRANSPORT=streamable-http CRONOMETER_PORT=<free port>` and
84
+ dummy credentials, connect with the `mcp` Python client's streamable HTTP transport, and call
85
+ `list_tools`.
86
+ - Confirm the same 27 tools (14 under `CRONOMETER_READ_ONLY=1`) come back as over stdio.
87
+
88
+ **DON'T**
89
+ - Don't call tools that hit Cronometer. `list_tools` proves the transport; the tools are already
90
+ tested.
91
+
92
+ **DONE WHEN**
93
+ - `list_tools` over HTTP returns 27 tool names and the server shuts down cleanly on SIGINT.
94
+
95
+ ## 3. README
96
+
97
+ **DO**
98
+ - Add a "Remote access (streamable HTTP)" subsection under *Configure your MCP client*, after
99
+ *Read-only mode*, with: the three variables and their defaults, the endpoint URL form
100
+ `http://<host>:<port>/mcp`, a minimal `docker run` example that passes credentials by
101
+ `-e` and publishes the port, and a Claude Code `.mcp.json` snippet using `"type": "http"` and
102
+ `"url"`.
103
+ - State plainly, in the same voice as the read-only caveat, that the endpoint has no
104
+ authentication: anyone who can reach the port can read and change the Cronometer account.
105
+ Keep it on localhost or behind something that authenticates (VPN, Tailscale, an authenticating
106
+ reverse proxy).
107
+ - Mention that pairing it with `CRONOMETER_READ_ONLY=1` limits what a remote client can do, with
108
+ the same caveat the read-only section already makes about the credentials themselves.
109
+
110
+ **DON'T**
111
+ - Don't ship a Dockerfile in this repo. The requester wants to write their own; a `docker run`
112
+ line against the PyPI package is enough.
113
+ - Don't document `sse` or any option §1 chose not to expose.
114
+
115
+ **DONE WHEN**
116
+ - Every variable named in the README matches the names and defaults in `server.py`.
117
+ - The README example commands run as written (checked by eye against §2's working invocation).
118
+
119
+ ## 4. Ship it
120
+
121
+ **DO**
122
+ - Run the Codex adversarial review on the unreleased diff (per project memory).
123
+ - Release `2.4.0` with `./release.sh 2.4.0` after Paul's go-ahead, with the `cphoskins` `gh`
124
+ account active, then create the GitHub release from the tag so the publish workflow runs.
125
+ - Reply on discussion #12 with the version, the three variables and the localhost-by-default
126
+ note, and ask the requester to confirm it works in their container.
127
+
128
+ **DON'T**
129
+ - Don't bump versions by hand or publish to the MCP Registry by hand (see `AGENTS.md`).
130
+ - Don't close the discussion until the requester has confirmed, or two weeks pass.
131
+
132
+ **DONE WHEN**
133
+ - `pip install cronometer-mcp==2.4.0` serves `/mcp` with `CRONOMETER_TRANSPORT=streamable-http`.
134
+ - Discussion #12 has the reply and a link to the release.
135
+
136
+ ## Out of scope
137
+
138
+ - Authentication (OAuth, bearer tokens). Revisit if a second person asks or if someone exposes
139
+ it publicly by mistake and reports it.
140
+ - `sse` transport, stateless mode, custom paths.
141
+ - A maintained Dockerfile or image.
@@ -1,6 +1,6 @@
1
1
  """Cronometer MCP Server — nutrition data from Cronometer via MCP."""
2
2
 
3
- __version__ = "2.2.0"
3
+ __version__ = "2.4.0"
4
4
 
5
5
  from .client import CronometerClient
6
6
  from .markdown import generate_food_log_md