foodguidecanada 3.0.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.
- foodguidecanada-3.0.0/.dockerignore +12 -0
- foodguidecanada-3.0.0/.github/workflows/ci.yml +66 -0
- foodguidecanada-3.0.0/.github/workflows/data-refresh.yml +114 -0
- foodguidecanada-3.0.0/.github/workflows/release.yml +91 -0
- foodguidecanada-3.0.0/.gitignore +29 -0
- foodguidecanada-3.0.0/CHANGELOG.md +31 -0
- foodguidecanada-3.0.0/CONTRIBUTING.md +262 -0
- foodguidecanada-3.0.0/Dockerfile +39 -0
- foodguidecanada-3.0.0/IMPLEMENTATIONS.MD +175 -0
- foodguidecanada-3.0.0/LICENSE +21 -0
- foodguidecanada-3.0.0/PKG-INFO +350 -0
- foodguidecanada-3.0.0/README.md +322 -0
- foodguidecanada-3.0.0/docker-compose.yml +15 -0
- foodguidecanada-3.0.0/docs/RELEASING.md +26 -0
- foodguidecanada-3.0.0/docs/data.md +78 -0
- foodguidecanada-3.0.0/docs/demo.gif +0 -0
- foodguidecanada-3.0.0/docs/demo.tape +43 -0
- foodguidecanada-3.0.0/docs/mcp.md +196 -0
- foodguidecanada-3.0.0/main.py +11 -0
- foodguidecanada-3.0.0/pyproject.toml +81 -0
- foodguidecanada-3.0.0/requirements.txt +8 -0
- foodguidecanada-3.0.0/scripts/check_manifest.py +48 -0
- foodguidecanada-3.0.0/src/foodguide/__init__.py +8 -0
- foodguidecanada-3.0.0/src/foodguide/api/__init__.py +5 -0
- foodguidecanada-3.0.0/src/foodguide/api/cnf.py +492 -0
- foodguidecanada-3.0.0/src/foodguide/api/cnf_api.py +287 -0
- foodguidecanada-3.0.0/src/foodguide/api/dri.py +574 -0
- foodguidecanada-3.0.0/src/foodguide/api/eer.py +593 -0
- foodguidecanada-3.0.0/src/foodguide/api/recipe.py +233 -0
- foodguidecanada-3.0.0/src/foodguide/builders/__init__.py +1 -0
- foodguidecanada-3.0.0/src/foodguide/builders/dri_eer.py +1135 -0
- foodguidecanada-3.0.0/src/foodguide/cli.py +1915 -0
- foodguidecanada-3.0.0/src/foodguide/cnf/__init__.py +1 -0
- foodguidecanada-3.0.0/src/foodguide/cnf/builder.py +489 -0
- foodguidecanada-3.0.0/src/foodguide/cnf/db.py +180 -0
- foodguidecanada-3.0.0/src/foodguide/cnf/mcp_source.py +213 -0
- foodguidecanada-3.0.0/src/foodguide/config.py +49 -0
- foodguidecanada-3.0.0/src/foodguide/data/dri_macronutrients.json +1049 -0
- foodguidecanada-3.0.0/src/foodguide/data/eer_equations.json +267 -0
- foodguidecanada-3.0.0/src/foodguide/data_manager.py +365 -0
- foodguidecanada-3.0.0/src/foodguide/db/__init__.py +8 -0
- foodguidecanada-3.0.0/src/foodguide/db/cnf_tools.py +1803 -0
- foodguidecanada-3.0.0/src/foodguide/db/connection.py +43 -0
- foodguidecanada-3.0.0/src/foodguide/db/dri_tools.py +1101 -0
- foodguidecanada-3.0.0/src/foodguide/db/eer_tools.py +456 -0
- foodguidecanada-3.0.0/src/foodguide/db/math_tools.py +1132 -0
- foodguidecanada-3.0.0/src/foodguide/db/queries.py +303 -0
- foodguidecanada-3.0.0/src/foodguide/db/schema.py +1378 -0
- foodguidecanada-3.0.0/src/foodguide/db/sql_engine.py +339 -0
- foodguidecanada-3.0.0/src/foodguide/models/__init__.py +4 -0
- foodguidecanada-3.0.0/src/foodguide/models/cnf_models.py +524 -0
- foodguidecanada-3.0.0/src/foodguide/models/db_models.py +29 -0
- foodguidecanada-3.0.0/src/foodguide/models/dri_models.py +272 -0
- foodguidecanada-3.0.0/src/foodguide/models/eer_models.py +172 -0
- foodguidecanada-3.0.0/src/foodguide/models/math_models.py +49 -0
- foodguidecanada-3.0.0/src/foodguide/pipeline/__init__.py +1 -0
- foodguidecanada-3.0.0/src/foodguide/pipeline/build_all.py +302 -0
- foodguidecanada-3.0.0/src/foodguide/pipeline/install.py +391 -0
- foodguidecanada-3.0.0/src/foodguide/pipeline/manifest.py +299 -0
- foodguidecanada-3.0.0/src/foodguide/recipes/__init__.py +1 -0
- foodguidecanada-3.0.0/src/foodguide/recipes/builder.py +93 -0
- foodguidecanada-3.0.0/src/foodguide/recipes/fetch.py +88 -0
- foodguidecanada-3.0.0/src/foodguide/recipes/parser.py +152 -0
- foodguidecanada-3.0.0/src/foodguide/recipes/source.py +191 -0
- foodguidecanada-3.0.0/src/foodguide/resources.py +117 -0
- foodguidecanada-3.0.0/src/foodguide/server.py +236 -0
- foodguidecanada-3.0.0/src/foodguide/utils/__init__.py +5 -0
- foodguidecanada-3.0.0/tests/cnf_fixture.py +114 -0
- foodguidecanada-3.0.0/tests/conftest.py +28 -0
- foodguidecanada-3.0.0/tests/fixtures/dri_eer/dri_macronutrients.json +1049 -0
- foodguidecanada-3.0.0/tests/fixtures/dri_eer/dri_minimal.html +164 -0
- foodguidecanada-3.0.0/tests/fixtures/dri_eer/eer_equations.json +267 -0
- foodguidecanada-3.0.0/tests/fixtures/dri_eer/eer_minimal.html +277 -0
- foodguidecanada-3.0.0/tests/fixtures/golden_cnf.json +6142 -0
- foodguidecanada-3.0.0/tests/fixtures/golden_recipes.json +206 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/README.md +28 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/SHA256SUMS +15 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/dri_macronutrients.html +1207 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/eer_equations.html +754 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/apple-berry-crisp.html +612 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/apple-nachos-peanut-butter-caramel-sauce.html +610 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/apple-sandwiches.html +612 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/baked-fish-chips.html +633 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/black-bean-enchiladas.html +655 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/carrot-potato-pancakes.html +591 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/colourful-quinoa-salad.html +582 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/egg-veggie-scramble.html +600 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/kale-chickpea-curry.html +645 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/okroshka-cold-summer-soup.html +628 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/slow-cooker-burrito-bowls.html +624 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes/tofu-masala-curry.html +591 -0
- foodguidecanada-3.0.0/tests/fixtures/pages/recipes_index.html +1471 -0
- foodguidecanada-3.0.0/tests/fixtures/recipes_html/broken.html +27 -0
- foodguidecanada-3.0.0/tests/fixtures/recipes_html/fruit-salad.html +42 -0
- foodguidecanada-3.0.0/tests/fixtures/recipes_html/index.html +53 -0
- foodguidecanada-3.0.0/tests/fixtures/recipes_html/no-tips.html +34 -0
- foodguidecanada-3.0.0/tests/fixtures/recipes_html/two-groups.html +48 -0
- foodguidecanada-3.0.0/tests/pages_fixture.py +310 -0
- foodguidecanada-3.0.0/tests/test_cli.py +74 -0
- foodguidecanada-3.0.0/tests/test_cnf_builder.py +244 -0
- foodguidecanada-3.0.0/tests/test_cnf_cli.py +221 -0
- foodguidecanada-3.0.0/tests/test_cnf_golden.py +123 -0
- foodguidecanada-3.0.0/tests/test_cnf_local_source.py +131 -0
- foodguidecanada-3.0.0/tests/test_data.py +23 -0
- foodguidecanada-3.0.0/tests/test_dri_eer_cli.py +246 -0
- foodguidecanada-3.0.0/tests/test_dri_eer_data_manager.py +135 -0
- foodguidecanada-3.0.0/tests/test_dri_eer_mcp.py +151 -0
- foodguidecanada-3.0.0/tests/test_dri_eer_parsers.py +352 -0
- foodguidecanada-3.0.0/tests/test_dri_eer_snapshot.py +170 -0
- foodguidecanada-3.0.0/tests/test_mcp_fixes.py +24 -0
- foodguidecanada-3.0.0/tests/test_offline_modules.py +74 -0
- foodguidecanada-3.0.0/tests/test_offline_subprocess.py +206 -0
- foodguidecanada-3.0.0/tests/test_pages_fixture.py +87 -0
- foodguidecanada-3.0.0/tests/test_pipeline_build_all.py +372 -0
- foodguidecanada-3.0.0/tests/test_pipeline_install.py +370 -0
- foodguidecanada-3.0.0/tests/test_pipeline_manifest.py +512 -0
- foodguidecanada-3.0.0/tests/test_recipes_cli.py +331 -0
- foodguidecanada-3.0.0/tests/test_recipes_golden.py +184 -0
- foodguidecanada-3.0.0/tests/test_recipes_mcp.py +151 -0
- foodguidecanada-3.0.0/tests/test_recipes_parser.py +222 -0
- foodguidecanada-3.0.0/tests/test_recipes_source.py +270 -0
- foodguidecanada-3.0.0/tests/test_server_import.py +13 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- name: Install uv
|
|
13
|
+
uses: astral-sh/setup-uv@v5
|
|
14
|
+
with:
|
|
15
|
+
enable-cache: true
|
|
16
|
+
- name: Install dependencies
|
|
17
|
+
run: uv sync
|
|
18
|
+
- name: Lint
|
|
19
|
+
run: uv run ruff check .
|
|
20
|
+
- name: Test
|
|
21
|
+
run: uv run pytest -q -m "not live"
|
|
22
|
+
- name: Build
|
|
23
|
+
run: uv build
|
|
24
|
+
- name: Smoke test entry point
|
|
25
|
+
run: uv run foodguide --help
|
|
26
|
+
|
|
27
|
+
docker:
|
|
28
|
+
# Builds the image (it downloads the data-latest release) and runs it the way docker-compose.yml does:
|
|
29
|
+
# read-only, no capabilities, 512 MB. Then talks to it over HTTP like a real client.
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: astral-sh/setup-uv@v5
|
|
34
|
+
- run: docker build -t foodguide-mcp .
|
|
35
|
+
- name: Run the container
|
|
36
|
+
run: |
|
|
37
|
+
docker run -d --name fg --read-only --tmpfs /tmp --cap-drop ALL \
|
|
38
|
+
--security-opt no-new-privileges:true --memory 512m \
|
|
39
|
+
-p 127.0.0.1:8000:8000 foodguide-mcp
|
|
40
|
+
for i in $(seq 1 40); do curl -fs -o /dev/null http://127.0.0.1:8000/mcp/ && break; sleep 2; done
|
|
41
|
+
curl -fsS -o /dev/null -w "GET /mcp/ -> %{http_code}\n" http://127.0.0.1:8000/mcp/
|
|
42
|
+
- name: Runs as a non-root user
|
|
43
|
+
run: test "$(docker exec fg id -u)" = "10001"
|
|
44
|
+
- name: Data is installed and verified
|
|
45
|
+
run: |
|
|
46
|
+
docker exec fg foodguide data status | tee status.txt
|
|
47
|
+
test "$(grep -c ': ok' status.txt)" -ge 5
|
|
48
|
+
- name: 24 tools over HTTP
|
|
49
|
+
run: |
|
|
50
|
+
uv run --with fastmcp python - <<'EOF'
|
|
51
|
+
import asyncio
|
|
52
|
+
from fastmcp import Client
|
|
53
|
+
async def main():
|
|
54
|
+
async with Client("http://127.0.0.1:8000/mcp") as c:
|
|
55
|
+
n = len(await c.list_tools())
|
|
56
|
+
print("tools:", n)
|
|
57
|
+
assert n == 24, n
|
|
58
|
+
r = await c.call_tool("search_and_get_cnf_macronutrients",
|
|
59
|
+
{"input_data": {"food_name": "lentils", "session_id": "ci", "max_results": 2}})
|
|
60
|
+
assert "Lentils" in str(r), r
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
EOF
|
|
63
|
+
- name: Container logs on failure
|
|
64
|
+
if: failure()
|
|
65
|
+
run: docker logs fg
|
|
66
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
name: Data refresh
|
|
2
|
+
|
|
3
|
+
# Rebuilds the datasets the CLI and MCP server read (CNF 2026 SQLite, recipes, DRI, EER) and
|
|
4
|
+
# publishes them as assets of the `data-latest` release. Runs weekly; publishes only when the
|
|
5
|
+
# content changed. `foodguide data fetch` installs from that release.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "17 9 * * 1" # Mondays 09:17 UTC
|
|
10
|
+
pull_request: # dry run: builds and checks everything, never publishes
|
|
11
|
+
paths:
|
|
12
|
+
- ".github/workflows/data-refresh.yml"
|
|
13
|
+
- "scripts/check_manifest.py"
|
|
14
|
+
- "src/foodguide/pipeline/**"
|
|
15
|
+
- "src/foodguide/builders/**"
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
force:
|
|
19
|
+
description: "Publish even if nothing changed"
|
|
20
|
+
type: boolean
|
|
21
|
+
default: false
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: write # release assets
|
|
25
|
+
issues: write # failure report
|
|
26
|
+
|
|
27
|
+
concurrency:
|
|
28
|
+
group: data-refresh
|
|
29
|
+
cancel-in-progress: false
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
refresh:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
timeout-minutes: 45
|
|
35
|
+
env:
|
|
36
|
+
GH_TOKEN: ${{ github.token }}
|
|
37
|
+
TAG: data-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
- uses: astral-sh/setup-uv@v5
|
|
41
|
+
with:
|
|
42
|
+
enable-cache: true
|
|
43
|
+
- run: uv sync
|
|
44
|
+
|
|
45
|
+
- name: Run the live test suite (full recipes crawl + snapshot comparisons)
|
|
46
|
+
run: uv run pytest -q -m live
|
|
47
|
+
|
|
48
|
+
- name: Fetch previous manifest
|
|
49
|
+
id: prev
|
|
50
|
+
run: |
|
|
51
|
+
mkdir -p prev
|
|
52
|
+
if gh release download "$TAG" -p manifest.json -D prev 2>/dev/null; then
|
|
53
|
+
echo "have=true" >> "$GITHUB_OUTPUT"
|
|
54
|
+
else
|
|
55
|
+
echo "have=false" >> "$GITHUB_OUTPUT"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
- name: Build all datasets
|
|
59
|
+
run: uv run foodguide data build all --download-cnf --out-dir out
|
|
60
|
+
|
|
61
|
+
- name: Sanity-check the build
|
|
62
|
+
run: |
|
|
63
|
+
if [ "${{ steps.prev.outputs.have }}" = "true" ]; then
|
|
64
|
+
uv run python scripts/check_manifest.py out/manifest.json prev/manifest.json
|
|
65
|
+
else
|
|
66
|
+
uv run python scripts/check_manifest.py out/manifest.json
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
- name: Did anything change?
|
|
70
|
+
id: changed
|
|
71
|
+
run: |
|
|
72
|
+
if [ "${{ steps.prev.outputs.have }}" != "true" ] || [ "${{ inputs.force }}" = "true" ]; then
|
|
73
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"; exit 0
|
|
74
|
+
fi
|
|
75
|
+
set +e
|
|
76
|
+
uv run foodguide data changed prev/manifest.json out/manifest.json | tee changed.txt
|
|
77
|
+
code=${PIPESTATUS[0]}
|
|
78
|
+
set -e
|
|
79
|
+
if [ "$code" = "0" ]; then echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
80
|
+
elif [ "$code" = "10" ]; then echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
81
|
+
else exit "$code"; fi
|
|
82
|
+
|
|
83
|
+
- name: Publish release assets
|
|
84
|
+
if: steps.changed.outputs.changed == 'true' && github.event_name != 'pull_request'
|
|
85
|
+
run: |
|
|
86
|
+
if ! gh release view "$TAG" >/dev/null 2>&1; then
|
|
87
|
+
gh release create "$TAG" --title "Data (latest)" \
|
|
88
|
+
--notes "Datasets built by the Data refresh workflow. Install with: foodguide data fetch"
|
|
89
|
+
fi
|
|
90
|
+
rm -f out/*.zip
|
|
91
|
+
gh release upload "$TAG" out/* --clobber
|
|
92
|
+
gh release edit "$TAG" --notes "Built $(date -u +%FT%TZ) from workflow run ${{ github.run_id }}. Install with: foodguide data fetch"
|
|
93
|
+
|
|
94
|
+
- name: Summary
|
|
95
|
+
if: always()
|
|
96
|
+
run: |
|
|
97
|
+
{
|
|
98
|
+
echo "## Data refresh"
|
|
99
|
+
echo "Published: ${{ steps.changed.outputs.changed || 'not reached' }}"
|
|
100
|
+
[ -f changed.txt ] && { echo '```'; cat changed.txt; echo '```'; }
|
|
101
|
+
[ -f out/manifest.json ] && { echo '```json'; cat out/manifest.json; echo '```'; }
|
|
102
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
103
|
+
|
|
104
|
+
- name: Report failure
|
|
105
|
+
if: failure() && github.event_name != 'pull_request'
|
|
106
|
+
run: |
|
|
107
|
+
title="Data refresh failed"
|
|
108
|
+
url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
109
|
+
existing=$(gh issue list --state open --search "\"$title\" in:title" --json number --jq '.[0].number')
|
|
110
|
+
if [ -n "$existing" ]; then
|
|
111
|
+
gh issue comment "$existing" --body "Failed again: $url"
|
|
112
|
+
else
|
|
113
|
+
gh issue create --title "$title" --body "The scheduled data refresh failed: $url. Likely cause: a source page changed. The previous release is still in place."
|
|
114
|
+
fi
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Push a tag like v3.0.0 (it must match the version in pyproject.toml). Builds the package, makes
|
|
4
|
+
# a GitHub release with the wheel and sdist, and publishes to PyPI with trusted publishing
|
|
5
|
+
# (no token: configure the "foodguidecanada" project's publisher on pypi.org once, see docs/RELEASING.md).
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
- run: uv sync
|
|
18
|
+
- run: uv run ruff check .
|
|
19
|
+
- run: uv run pytest -q -m "not live"
|
|
20
|
+
- name: Tag matches the package version
|
|
21
|
+
run: |
|
|
22
|
+
v=$(uv run python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
23
|
+
[ "v$v" = "${GITHUB_REF_NAME}" ] || { echo "tag ${GITHUB_REF_NAME} != package version $v"; exit 1; }
|
|
24
|
+
- run: uv build
|
|
25
|
+
- run: uvx twine check dist/*
|
|
26
|
+
- uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
|
|
31
|
+
github-release:
|
|
32
|
+
needs: build
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
permissions:
|
|
35
|
+
contents: write
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- uses: actions/download-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist
|
|
42
|
+
- name: Release notes from CHANGELOG.md
|
|
43
|
+
run: |
|
|
44
|
+
v="${GITHUB_REF_NAME#v}"
|
|
45
|
+
awk -v v="$v" '$0 ~ "^## "v" " {f=1; next} /^## / {f=0} f' CHANGELOG.md > notes.md
|
|
46
|
+
[ -s notes.md ] || echo "Release $v" > notes.md
|
|
47
|
+
- name: Create the release
|
|
48
|
+
env:
|
|
49
|
+
GH_TOKEN: ${{ github.token }}
|
|
50
|
+
run: |
|
|
51
|
+
v="${GITHUB_REF_NAME#v}"
|
|
52
|
+
gh release create "$GITHUB_REF_NAME" dist/* --title "MCP${v%.*}" --notes-file notes.md
|
|
53
|
+
|
|
54
|
+
pypi:
|
|
55
|
+
needs: build
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
environment: pypi
|
|
58
|
+
permissions:
|
|
59
|
+
id-token: write
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/download-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: dist
|
|
64
|
+
path: dist
|
|
65
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
66
|
+
|
|
67
|
+
docker:
|
|
68
|
+
needs: build
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
permissions:
|
|
71
|
+
contents: read
|
|
72
|
+
packages: write
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
- uses: docker/setup-buildx-action@v3
|
|
76
|
+
- uses: docker/login-action@v3
|
|
77
|
+
with:
|
|
78
|
+
registry: ghcr.io
|
|
79
|
+
username: ${{ github.actor }}
|
|
80
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
81
|
+
- name: Image tags
|
|
82
|
+
id: meta
|
|
83
|
+
run: |
|
|
84
|
+
v="${GITHUB_REF_NAME#v}"
|
|
85
|
+
repo=$(echo "ghcr.io/${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')
|
|
86
|
+
echo "tags=$repo:$v,$repo:latest" >> "$GITHUB_OUTPUT"
|
|
87
|
+
- uses: docker/build-push-action@v6
|
|
88
|
+
with:
|
|
89
|
+
context: .
|
|
90
|
+
push: true
|
|
91
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
|
|
10
|
+
# Tooling caches
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
|
|
14
|
+
# Local databases (never commit)
|
|
15
|
+
*.db
|
|
16
|
+
*.sqlite
|
|
17
|
+
*.sqlite3
|
|
18
|
+
|
|
19
|
+
# Secrets / local env
|
|
20
|
+
.env
|
|
21
|
+
.env.*
|
|
22
|
+
|
|
23
|
+
# Scrape caches and logs
|
|
24
|
+
cache/
|
|
25
|
+
logs/
|
|
26
|
+
uv.lock
|
|
27
|
+
|
|
28
|
+
# macOS
|
|
29
|
+
.DS_Store
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 3.0.0 (2026-11-02)
|
|
4
|
+
|
|
5
|
+
A rewrite around an installable package and offline data. The old `v2.0` was the MCP server alone.
|
|
6
|
+
|
|
7
|
+
- **Package:** published as `foodguidecanada` (import name and commands stay `foodguide`): a typer CLI
|
|
8
|
+
(`foodguide`) and the MCP server (`foodguide-mcp`, install with the `mcp` extra). The CLI install is light (no pandas, no fastmcp).
|
|
9
|
+
- **Offline data, no network at run time:** Canadian Nutrient File 2026 as SQLite (EN/FR full-text search),
|
|
10
|
+
the Food Guide recipes (141, from the new canada.ca pages), Health Canada DRI and EER tables.
|
|
11
|
+
- **CLI:** `cnf`, `recipes`, `dri`, `eer`, `data` (build, status, fetch, manifest, changed).
|
|
12
|
+
- **Data pipeline:** `foodguide data build all` plus a weekly GitHub Actions workflow that rebuilds the
|
|
13
|
+
datasets and publishes them to the `data-latest` release only when the content changed.
|
|
14
|
+
- **MCP fixes:** state folder is created, session tables are created at startup, `list_filters` validates,
|
|
15
|
+
recipe tools work again against the local data.
|
|
16
|
+
- **Tests:** no mocks. Real saved pages, local servers and real subprocesses; golden answer keys made by a
|
|
17
|
+
separate script.
|
|
18
|
+
- **Breaking changes for MCP clients (recipes):**
|
|
19
|
+
- `search_recipes`: `max_pages` is replaced by `limit` (default 20); it returns an object
|
|
20
|
+
`{recipes, count, filters_applied, filters_ignored, ...}` instead of a list.
|
|
21
|
+
- `fruits`, `vegetables`, `proteins`, `whole_grains` and `appliances` filters no longer exist on the
|
|
22
|
+
new canada.ca pages; they are reported in `filters_ignored`. Use `search_text`, `meals` and
|
|
23
|
+
`collections`.
|
|
24
|
+
- `get_recipe`: the `url` argument is now `url_or_slug` (a slug or a canada.ca recipe URL).
|
|
25
|
+
- `list_filters` returns `{group: {tag: label}}`.
|
|
26
|
+
- CNF tools read the local SQLite database. Run `foodguide data fetch` (or `data build cnf`) first.
|
|
27
|
+
- **Security default:** the MCP HTTP transport now listens on `127.0.0.1` (it was `0.0.0.0` with no
|
|
28
|
+
authentication). Use `--host` or `HOST` to expose it deliberately.
|
|
29
|
+
- **Docker:** a `Dockerfile` and `docker-compose.yml` for the HTTP MCP server (pinned base image, non-root,
|
|
30
|
+
read-only, data baked in), and images published to GHCR on release.
|
|
31
|
+
- **Removed:** the live scrapers of the old `food-guide.canada.ca` host and `uv.lock`.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Contributing to foodguide
|
|
2
|
+
|
|
3
|
+
`foodguide` is a command-line tool and an MCP server for Canada's Food Guide data. It reads the
|
|
4
|
+
Canadian Nutrient File (CNF) 2026, the Food Guide recipes, and the Health Canada DRI and EER
|
|
5
|
+
tables. All data is read from local files at run time. The MCP server exposes 46 tools on the
|
|
6
|
+
stdio transport.
|
|
7
|
+
|
|
8
|
+
Bug fixes, docs and new features are welcome. This page says how to set up, test and change the
|
|
9
|
+
code. For how the code fits together, read [IMPLEMENTATIONS.MD](IMPLEMENTATIONS.MD).
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
You need Python 3.12 or newer, [uv](https://docs.astral.sh/uv/) and Git.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/Aryan-Jhaveri/mcp-foodguidecanada.git
|
|
17
|
+
cd mcp-foodguidecanada
|
|
18
|
+
uv sync
|
|
19
|
+
uv run foodguide --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`uv sync` installs the package in editable mode and the dev group. The dev group includes pytest,
|
|
23
|
+
ruff and the `mcp` extra (`foodguidecanada[mcp]`), so the MCP server tests can run.
|
|
24
|
+
|
|
25
|
+
Datasets are not in the repo. The CLI reads them from `~/.local/share/foodguide` (override with
|
|
26
|
+
`FOODGUIDE_DATA_DIR`). Install them with `uv run foodguide data fetch`, or build them yourself
|
|
27
|
+
(see "Change a dataset builder" below). The bundled DRI and EER JSON files in
|
|
28
|
+
`src/foodguide/data/` are the fallback for those two datasets.
|
|
29
|
+
|
|
30
|
+
## Workflow
|
|
31
|
+
|
|
32
|
+
1. Make a branch: `git checkout -b feat/short-name` or `fix/short-name`.
|
|
33
|
+
2. Change the code. Add or update a test.
|
|
34
|
+
3. Run lint and the tests (below).
|
|
35
|
+
4. Commit with a clear message: `feat:`, `fix:`, `docs:`, `refactor:`, `test:` or `perf:`.
|
|
36
|
+
5. Push and open a pull request.
|
|
37
|
+
|
|
38
|
+
## Lint
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv run ruff check .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Settings are in `pyproject.toml`: line length 100, rules E, F, W and I. E501 is ignored because
|
|
45
|
+
older code has long data-table lines; keep new code under 100 characters.
|
|
46
|
+
`src/foodguide/server.py` ignores E402 on purpose: it sets warning filters before it imports
|
|
47
|
+
fastmcp.
|
|
48
|
+
|
|
49
|
+
## Tests
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv run pytest -q -m "not live" # what CI runs: fast, no internet needed
|
|
53
|
+
uv run pytest -q -m live # slow: fetches live canada.ca pages
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `-m "not live"` is the CI run (`.github/workflows/ci.yml`, after `ruff check .`).
|
|
57
|
+
- `-m live` runs the full 141-page recipes crawl and the live DRI/EER snapshot comparisons. The
|
|
58
|
+
weekly data-refresh workflow (`.github/workflows/data-refresh.yml`) runs it too.
|
|
59
|
+
- `FOODGUIDE_SKIP_LIVE_TESTS=1` skips the live DRI/EER snapshot comparisons
|
|
60
|
+
(`tests/test_dri_eer_snapshot.py`), even in a `-m live` run. It does not skip the live recipes
|
|
61
|
+
crawl.
|
|
62
|
+
- Tests that start a local `http.server` skip cleanly in sandboxes that block loopback TCP
|
|
63
|
+
connections. They run in CI.
|
|
64
|
+
- `tests/test_cnf_golden.py` needs the CNF 2026 zip. It skips if the zip is missing. Point it at
|
|
65
|
+
your copy with `FOODGUIDE_TEST_CNF_ZIP`. The live recipes test can use a folder of crawled
|
|
66
|
+
pages in `FOODGUIDE_TEST_RECIPE_CACHE`.
|
|
67
|
+
|
|
68
|
+
### Tests: no mocks (project rule)
|
|
69
|
+
|
|
70
|
+
We do **not** mock our tests. The suite runs against real inputs: real saved
|
|
71
|
+
canada.ca pages (`tests/fixtures/pages/`, served by a real localhost
|
|
72
|
+
`http.server` from the `pages_server` fixture), real files, a real SQLite
|
|
73
|
+
database, real subprocess runs of the CLI, and answer keys
|
|
74
|
+
(`tests/fixtures/golden_*.json`) produced by separate, independent scripts
|
|
75
|
+
that share no code with the package. Never edit an answer key by hand.
|
|
76
|
+
|
|
77
|
+
**Allowed**
|
|
78
|
+
|
|
79
|
+
- `monkeypatch.setenv` / `monkeypatch.delenv` / `monkeypatch.chdir`. These set
|
|
80
|
+
the environment, they fake nothing. The production code reads all its
|
|
81
|
+
configuration points from the environment at call time
|
|
82
|
+
(`FOODGUIDE_DATA_DIR`, `FOODGUIDE_DB_FILE`, `FOODGUIDE_STATE_DIR`,
|
|
83
|
+
`FOODGUIDE_SOURCE_BASE_URL`, `FOODGUIDE_CKAN_PACKAGE_URL`,
|
|
84
|
+
`FOODGUIDE_RECIPES_CACHE_DIR`, `SOURCE_DATE_EPOCH`), so tests never need to
|
|
85
|
+
patch attributes.
|
|
86
|
+
- A real `http.server.ThreadingHTTPServer` on 127.0.0.1 in a background thread
|
|
87
|
+
(`tests/pages_fixture.py`), including "broken" responses derived from real
|
|
88
|
+
pages (truncated HTML, a page with its Ingredients heading removed, HTTP 500).
|
|
89
|
+
- Answer keys from independent scripts; build options such as
|
|
90
|
+
`data build recipes --max-recipes/--only` to work with a small fixture set.
|
|
91
|
+
|
|
92
|
+
**Not allowed**
|
|
93
|
+
|
|
94
|
+
- `unittest.mock`, `MagicMock`, `pytest`'s `patch`, and any
|
|
95
|
+
`monkeypatch.setattr` / `monkeypatch.setitem` on code under test or on
|
|
96
|
+
libraries (`requests`, `datetime`, builders, fetchers).
|
|
97
|
+
- "Network is blocked" fixtures that swap `requests.Session.request` for a
|
|
98
|
+
raiser. Prove offline-ness instead with the static import check
|
|
99
|
+
(`tests/test_offline_modules.py`) and real subprocess runs with
|
|
100
|
+
`HTTP_PROXY`/`HTTPS_PROXY` pointed at a dead port
|
|
101
|
+
(`tests/test_offline_subprocess.py`).
|
|
102
|
+
|
|
103
|
+
### Golden answer keys
|
|
104
|
+
|
|
105
|
+
- `tests/fixtures/golden_cnf.json`: CNF foods, nutrient values and measures, taken straight from the
|
|
106
|
+
CNF 2026 zip. Used by `tests/test_cnf_golden.py`.
|
|
107
|
+
- `tests/fixtures/golden_recipes.json`: recipe titles, ingredients, directions, times and servings.
|
|
108
|
+
Used by `tests/test_recipes_golden.py`.
|
|
109
|
+
|
|
110
|
+
The tests compare CLI output against these keys. The keys are checked in. The generator scripts
|
|
111
|
+
are **not** in this repo: they were written separately on purpose, so that they share no code with
|
|
112
|
+
the package, and there is no command here that regenerates them. Do not edit the files by hand. If
|
|
113
|
+
upstream data changes and a golden test fails, raise it in the pull request and regenerate the
|
|
114
|
+
key with an independent script that reads the source directly (the CNF zip or the canada.ca
|
|
115
|
+
pages), then commit the new key.
|
|
116
|
+
|
|
117
|
+
## Add a CLI command
|
|
118
|
+
|
|
119
|
+
The CLI is one typer app in `src/foodguide/cli.py`.
|
|
120
|
+
|
|
121
|
+
- `app` is the root app. Each group is its own `typer.Typer` (`cnf_app`, `dri_app`, `eer_app`,
|
|
122
|
+
`recipes_app`, `data_app`, and `data_build_app` under `data_app`), attached with
|
|
123
|
+
`app.add_typer(...)`.
|
|
124
|
+
- A command is a function with `@<group>_app.command("name")`.
|
|
125
|
+
- The global `--format table|csv|json` option is the `OutputFormat` enum. It is stored in
|
|
126
|
+
`ctx.obj["format"]`. Commands also accept their own `--format` and fall back to the global one:
|
|
127
|
+
`fmt = format if format is not None else ctx.obj["format"]`.
|
|
128
|
+
- **stdout is data only.** Print results with `typer.echo` (or `csv.writer(sys.stdout)`). Print
|
|
129
|
+
log lines, warnings and errors with `_log(message)`, which writes to stderr.
|
|
130
|
+
- Exit codes: `raise typer.Exit(code=0)` on success, `1` on an error, `2` for bad usage. The
|
|
131
|
+
exception is `data changed`, which exits `10` when datasets differ.
|
|
132
|
+
|
|
133
|
+
Steps:
|
|
134
|
+
|
|
135
|
+
1. Add `@<group>_app.command("name")` in `src/foodguide/cli.py`, with a docstring and an example.
|
|
136
|
+
2. Support the three output formats. Keep stdout data-only.
|
|
137
|
+
3. Add a test in `tests/` that runs the command against real data (the CLI tests use
|
|
138
|
+
`typer.testing.CliRunner` or a real subprocess).
|
|
139
|
+
4. Run `uv run ruff check .` and `uv run pytest -q -m "not live"`.
|
|
140
|
+
5. Update the command list in README.md.
|
|
141
|
+
|
|
142
|
+
## Add an MCP tool
|
|
143
|
+
|
|
144
|
+
Tools are registered in `src/foodguide/server.py` through the `register_*` functions in
|
|
145
|
+
`src/foodguide/db/` and `src/foodguide/api/`, with `@mcp.tool()`. Tools that need SQLite session
|
|
146
|
+
state are only registered on the stdio transport. Add a test in `tests/` (see
|
|
147
|
+
`tests/test_dri_eer_mcp.py` and `tests/test_recipes_mcp.py`).
|
|
148
|
+
|
|
149
|
+
## Change a dataset builder
|
|
150
|
+
|
|
151
|
+
Builders live here:
|
|
152
|
+
|
|
153
|
+
- DRI and EER: `src/foodguide/builders/dri_eer.py`
|
|
154
|
+
- CNF: `src/foodguide/cnf/builder.py` (reads the CNF zip from open.canada.ca)
|
|
155
|
+
- Recipes: `src/foodguide/recipes/` (`fetch.py`, `parser.py`, `builder.py`)
|
|
156
|
+
|
|
157
|
+
Rules for anything that fetches a page:
|
|
158
|
+
|
|
159
|
+
- Send the identifying User-Agent
|
|
160
|
+
`foodguide-data-build/0.1 (+https://github.com/Aryan-Jhaveri/mcp-foodguidecanada)`. canada.ca
|
|
161
|
+
stalls plain `Mozilla/5.0` agents. Do not change it.
|
|
162
|
+
- Wait at least 1 second between fetches (`PoliteFetcher`).
|
|
163
|
+
- Retry with backoff on network errors and 5xx responses.
|
|
164
|
+
- Build page URLs from `source_base_url()` in `src/foodguide/config.py`. It returns
|
|
165
|
+
`FOODGUIDE_SOURCE_BASE_URL`, default `https://www.canada.ca`. Tests use this to point the
|
|
166
|
+
builders at the local fixture server. Never hard-code the host.
|
|
167
|
+
|
|
168
|
+
After you change a builder:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv run foodguide data build dri # or: eer, recipes, cnf, all
|
|
172
|
+
uv run pytest -q -m "not live"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
If the output changed, regenerate the golden key with its independent generator (see "Golden
|
|
176
|
+
answer keys") and explain the change in the pull request. For a full build into a clean folder
|
|
177
|
+
with a manifest, use `uv run foodguide data build all --out-dir out`. Use `--download-cnf` to
|
|
178
|
+
download the CNF zip, or `--cnf-zip PATH`.
|
|
179
|
+
|
|
180
|
+
## Code standards
|
|
181
|
+
|
|
182
|
+
Follow the patterns already in `src/foodguide`. `uv run ruff check .` must pass.
|
|
183
|
+
|
|
184
|
+
- **Type hints** on every function parameter and return value. Use `Optional[X]` for values that can be
|
|
185
|
+
`None`, and `List[...]` and `Dict[str, Any]` for collections.
|
|
186
|
+
- **Docstrings:** a short module docstring at the top of each file, and a docstring on each public
|
|
187
|
+
function that says what it does and what it returns. MCP tool docstrings are the text the model
|
|
188
|
+
reads, so write them for the model: say when to use the tool and what the arguments mean.
|
|
189
|
+
- **Comments:** use `#` for field explanations in dataclasses.
|
|
190
|
+
- **Errors:** return a clear message instead of raising into the client. MCP tools return
|
|
191
|
+
`{"error": "..."}`, and CLI commands print the message to stderr and exit 1.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
def get_recipe(url_or_slug: str) -> Dict[str, Any]:
|
|
195
|
+
"""Return one recipe by slug or canada.ca URL."""
|
|
196
|
+
try:
|
|
197
|
+
recipe = _get_source().get_recipe(url_or_slug)
|
|
198
|
+
except RuntimeError as exc:
|
|
199
|
+
return {"error": str(exc)}
|
|
200
|
+
if recipe is None:
|
|
201
|
+
return {"error": f"Recipe '{url_or_slug}' not found in local data."}
|
|
202
|
+
return recipe
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- **Offline modules stay offline.** Code that runs when a user asks a question (CLI readers, MCP tools)
|
|
206
|
+
must not import `requests` or `urllib.request`. Only the builders and `data fetch` touch the network.
|
|
207
|
+
`tests/test_offline_modules.py` checks this.
|
|
208
|
+
|
|
209
|
+
## Commit messages
|
|
210
|
+
|
|
211
|
+
Use a short prefix and a clear sentence:
|
|
212
|
+
|
|
213
|
+
- `feat:` new feature
|
|
214
|
+
- `fix:` bug fix
|
|
215
|
+
- `docs:` documentation
|
|
216
|
+
- `refactor:` code change with no behaviour change
|
|
217
|
+
- `test:` adding or fixing tests
|
|
218
|
+
- `perf:` performance
|
|
219
|
+
|
|
220
|
+
## Pull requests
|
|
221
|
+
|
|
222
|
+
Before you open one:
|
|
223
|
+
|
|
224
|
+
- `uv run ruff check .` passes.
|
|
225
|
+
- `uv run pytest -q -m "not live"` passes.
|
|
226
|
+
- Docs are updated if behaviour changed (README.md, IMPLEMENTATIONS.MD).
|
|
227
|
+
- If you tested the MCP server, say which client and transport.
|
|
228
|
+
|
|
229
|
+
A pull request should say what changed and why, the type of change (fix, feature, breaking, docs),
|
|
230
|
+
how you tested it, and any issue it closes. Maintainers review within a few days.
|
|
231
|
+
|
|
232
|
+
## Releases
|
|
233
|
+
|
|
234
|
+
Maintainers cut releases. The steps (version bump, tag `vX.Y.Z`, PyPI trusted publishing) are in
|
|
235
|
+
[docs/RELEASING.md](docs/RELEASING.md). The PyPI name is `foodguidecanada`. The import package and
|
|
236
|
+
commands stay `foodguide` and `foodguide-mcp`.
|
|
237
|
+
|
|
238
|
+
## Where help is wanted
|
|
239
|
+
|
|
240
|
+
- **Fewer tool calls.** Merge `calculate_recipe_nutrition_summary` and `query_recipe_macros_table`,
|
|
241
|
+
and `store_recipe_in_temp_tables` and `simple_recipe_setup`, so a model needs fewer steps.
|
|
242
|
+
- **Smaller files.** Split `db/cnf_tools.py` and move helpers into utility modules.
|
|
243
|
+
- **Diagrams.** A Mermaid diagram of the data flow and of how the tools work together.
|
|
244
|
+
- **More nutrition.** Micronutrients, vitamins and minerals in the DRI tables and the adequacy tools.
|
|
245
|
+
- **Easier install.** A Dockerfile for the HTTP transport, Smithery or npm packaging.
|
|
246
|
+
- **Ideas:** weekly meal planning, tracking ingredients in stock, personal nutrition goals, more
|
|
247
|
+
export formats.
|
|
248
|
+
|
|
249
|
+
Check [IMPLEMENTATIONS.MD](IMPLEMENTATIONS.MD) for the current list before you start, and open an
|
|
250
|
+
issue first for anything large.
|
|
251
|
+
|
|
252
|
+
## Community
|
|
253
|
+
|
|
254
|
+
Be respectful, collaborative, constructive and patient. Use GitHub Issues for bugs and feature
|
|
255
|
+
requests, with clear titles and steps to reproduce. When you discuss nutrition behaviour, link the
|
|
256
|
+
Health Canada page it comes from.
|
|
257
|
+
|
|
258
|
+
## Data credits
|
|
259
|
+
|
|
260
|
+
- Canadian Nutrient File 2026, Health Canada, under the Open Government Licence – Canada.
|
|
261
|
+
- Food Guide recipes, Crown copyright, Health Canada.
|
|
262
|
+
- Dietary Reference Intakes (DRI) and Estimated Energy Requirement (EER) tables, Health Canada.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# foodguidecanada MCP server over HTTP, with the datasets baked into the image.
|
|
2
|
+
#
|
|
3
|
+
# docker build -t foodguide-mcp .
|
|
4
|
+
# docker run --rm -p 127.0.0.1:8000:8000 --read-only --tmpfs /tmp --memory 512m foodguide-mcp
|
|
5
|
+
# endpoint: http://127.0.0.1:8000/mcp
|
|
6
|
+
#
|
|
7
|
+
# Lessons from running containers on a real server: pinned base image, non-root user, read-only
|
|
8
|
+
# root filesystem, a memory limit, published on loopback only (put TLS and auth in front for other
|
|
9
|
+
# people), and no network at run time (the data is fetched once, at build time).
|
|
10
|
+
#
|
|
11
|
+
# python:3.12-slim-bookworm, pinned by digest (2026-11-02).
|
|
12
|
+
FROM python:3.12-slim-bookworm@sha256:e697c81897302a4c4333bade000d98038fbb21eb064f8ee1fd42d02ebcd9d337
|
|
13
|
+
|
|
14
|
+
LABEL org.opencontainers.image.source="https://github.com/Aryan-Jhaveri/mcp-foodguidecanada" \
|
|
15
|
+
org.opencontainers.image.description="Canada's Food Guide MCP server (HTTP) with offline data" \
|
|
16
|
+
org.opencontainers.image.licenses="MIT"
|
|
17
|
+
|
|
18
|
+
# Which release of the datasets to bake in (the Data refresh workflow publishes them).
|
|
19
|
+
ARG DATA_TAG=data-latest
|
|
20
|
+
|
|
21
|
+
RUN useradd --create-home --uid 10001 app
|
|
22
|
+
WORKDIR /src
|
|
23
|
+
COPY pyproject.toml README.md LICENSE ./
|
|
24
|
+
COPY src ./src
|
|
25
|
+
RUN pip install --no-cache-dir '.[mcp]' && rm -rf /src
|
|
26
|
+
|
|
27
|
+
USER app
|
|
28
|
+
ENV FOODGUIDE_DATA_DIR=/home/app/data \
|
|
29
|
+
MCP_TRANSPORT=http \
|
|
30
|
+
HOST=0.0.0.0 \
|
|
31
|
+
PORT=8000 \
|
|
32
|
+
PYTHONUNBUFFERED=1
|
|
33
|
+
# Downloads from GitHub over HTTPS and checks every file against the release manifest.
|
|
34
|
+
RUN foodguide data fetch --tag "${DATA_TAG}" && foodguide data status | head -3
|
|
35
|
+
|
|
36
|
+
EXPOSE 8000
|
|
37
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
|
|
38
|
+
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/mcp/', timeout=4).status == 200 else 1)"
|
|
39
|
+
CMD ["foodguide-mcp"]
|