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.
- cronometer_mcp-2.4.0/.github/workflows/publish.yml +79 -0
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/.gitignore +3 -0
- cronometer_mcp-2.4.0/AGENTS.md +15 -0
- cronometer_mcp-2.4.0/CLAUDE.md +1 -0
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/PKG-INFO +64 -2
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/README.md +62 -0
- cronometer_mcp-2.4.0/SPEC-streamable-http.md +141 -0
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/cronometer_mcp/__init__.py +1 -1
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/cronometer_mcp/client.py +241 -295
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/cronometer_mcp/server.py +121 -30
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/pyproject.toml +3 -4
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/release.sh +2 -2
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/server.json +2 -2
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/tests/test_client.py +247 -69
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/tests/test_diary_groups.py +15 -0
- cronometer_mcp-2.4.0/tests/test_server.py +193 -0
- cronometer_mcp-2.2.0/.github/workflows/publish.yml +0 -28
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/LICENSE +0 -0
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/RESEARCH-copy-repeat-gwt-rpc.md +0 -0
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/SPEC-diary-groups.md +0 -0
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/cronometer_mcp/markdown.py +0 -0
- {cronometer_mcp-2.2.0 → cronometer_mcp-2.4.0}/tests/__init__.py +0 -0
- {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
|
|
@@ -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.
|
|
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<
|
|
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.
|