mcp-server-brewfather 0.1.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.
@@ -0,0 +1,4 @@
1
+ # Brewfather API credentials — generate in Brewfather: Settings → API → Generate API Key.
2
+ # Grant only the scopes you want this server to have (see README "Security posture").
3
+ export BREWFATHER_USER_ID=your_user_id
4
+ export BREWFATHER_API_KEY=your_api_key
@@ -0,0 +1,14 @@
1
+ # Keep GitHub Actions pinned to immutable SHAs current. The workflows
2
+ # pin actions to commit SHAs (not floating tags); Dependabot opens PRs
3
+ # that bump both the SHA and the trailing `# vX.Y.Z` comment when new
4
+ # releases land.
5
+ version: 2
6
+ updates:
7
+ - package-ecosystem: github-actions
8
+ directory: /
9
+ schedule:
10
+ interval: weekly
11
+ - package-ecosystem: uv
12
+ directory: /
13
+ schedule:
14
+ interval: weekly
@@ -0,0 +1,119 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ # PR title validation - must NOT use conventional commit format
14
+ # (conventional format is for commit messages only; PR titles are descriptive)
15
+ pr-title:
16
+ name: PR Title Check
17
+ runs-on: ubuntu-latest
18
+ if: github.event_name == 'pull_request'
19
+ steps:
20
+ - name: Harden Runner
21
+ uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
22
+ with:
23
+ egress-policy: audit
24
+
25
+ - name: Check PR title is not conventional commit format
26
+ env:
27
+ PR_TITLE: ${{ github.event.pull_request.title }}
28
+ run: |
29
+ # Exempt release-please and Dependabot PRs
30
+ if echo "$PR_TITLE" | grep -qE "^chore\(main\): release [0-9]"; then
31
+ echo "✅ Release-please PR exempt"
32
+ exit 0
33
+ fi
34
+ if echo "$PR_TITLE" | grep -qE "^chore\(deps(-dev)?\): [Bb]ump "; then
35
+ echo "✅ Dependabot PR exempt"
36
+ exit 0
37
+ fi
38
+
39
+ PATTERN="^(feat|fix|perf|docs|refactor|ci|chore)(\(.+\))?!?:"
40
+ if echo "$PR_TITLE" | grep -qE "$PATTERN"; then
41
+ echo "::error::PR title should NOT use conventional commit format."
42
+ echo "::error::Use a descriptive title (e.g., 'Add reorder support' not 'feat: add reorder support')."
43
+ exit 1
44
+ fi
45
+ echo "✅ PR title format is valid"
46
+
47
+ lint:
48
+ name: Lint
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - name: Harden Runner
52
+ uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
53
+ with:
54
+ egress-policy: audit
55
+
56
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
57
+
58
+ - name: Set up uv
59
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
60
+ with:
61
+ enable-cache: true
62
+
63
+ - name: Install dependencies
64
+ run: uv sync --locked
65
+
66
+ - name: Ruff lint
67
+ run: uv run ruff check .
68
+
69
+ - name: Ruff format check
70
+ run: uv run ruff format --check .
71
+
72
+ test:
73
+ name: Test
74
+ runs-on: ubuntu-latest
75
+ steps:
76
+ - name: Harden Runner
77
+ uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
78
+ with:
79
+ egress-policy: audit
80
+
81
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
82
+
83
+ - name: Set up uv
84
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
85
+ with:
86
+ enable-cache: true
87
+
88
+ - name: Install dependencies
89
+ run: uv sync --locked
90
+
91
+ # Unit tests only. The acceptance suite hits the live Brewfather API and needs
92
+ # user credentials, so it is intentionally not run in CI (stays local/manual).
93
+ - name: Run unit tests
94
+ run: uv run pytest
95
+
96
+ # CI gate job for simplified branch protection — this is the "CI Success"
97
+ # status check referenced by the repository ruleset.
98
+ ci-success:
99
+ name: CI Success
100
+ runs-on: ubuntu-latest
101
+ needs: [pr-title, lint, test]
102
+ if: always()
103
+ steps:
104
+ - name: Harden Runner
105
+ uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
106
+ with:
107
+ egress-policy: audit
108
+
109
+ - name: Check all jobs passed
110
+ run: |
111
+ if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
112
+ echo "❌ Some jobs failed"
113
+ exit 1
114
+ fi
115
+ if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
116
+ echo "❌ Some jobs were cancelled"
117
+ exit 1
118
+ fi
119
+ echo "✅ All CI jobs passed"
@@ -0,0 +1,76 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ # Authenticate via a dedicated GitHub App (token minted at job time) rather
9
+ # than GITHUB_TOKEN. Events generated by the default GITHUB_TOKEN do not
10
+ # trigger downstream workflows, so the tag release-please pushes would never
11
+ # run release.yml (PyPI publish). App-minted tokens are treated as first-class
12
+ # actors and do trigger downstream workflows.
13
+ permissions: {}
14
+
15
+ jobs:
16
+ release-please:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - name: Harden Runner
20
+ uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
21
+ with:
22
+ egress-policy: audit
23
+
24
+ - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
25
+ id: app-token
26
+ with:
27
+ client-id: ${{ secrets.RELEASE_PLEASE_APP_CLIENT_ID }}
28
+ private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
29
+
30
+ - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
31
+ with:
32
+ token: ${{ steps.app-token.outputs.token }}
33
+ config-file: release-please-config.json
34
+ manifest-file: .release-please-manifest.json
35
+
36
+ # release-please bumps the version in pyproject.toml but not uv.lock, and
37
+ # the lock records the root project's own version, so `uv sync --locked`
38
+ # fails on the release PR — and on main once it merges.
39
+ #
40
+ # Find the open release PR directly rather than using the action's `pr`
41
+ # output: that output is set only when release-please creates or updates
42
+ # a PR. A commit of a hidden changelog type (ci, chore, docs) leaves the
43
+ # PR unchanged — no output — while still rebuilding the release branch on
44
+ # top of the new main, which drops any earlier lock-sync commit.
45
+ #
46
+ # `--repo` is required: no checkout has happened, so gh has no remote.
47
+ - id: release_pr
48
+ env:
49
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
50
+ run: |
51
+ branch=$(gh pr list --repo "${{ github.repository }}" --state open --limit 100 --json headRefName \
52
+ --jq '[.[] | select(.headRefName | startswith("release-please--"))][0].headRefName // ""')
53
+ echo "branch=$branch" >> "$GITHUB_OUTPUT"
54
+
55
+ - if: ${{ steps.release_pr.outputs.branch }}
56
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
57
+ with:
58
+ ref: ${{ steps.release_pr.outputs.branch }}
59
+ token: ${{ steps.app-token.outputs.token }}
60
+
61
+ - if: ${{ steps.release_pr.outputs.branch }}
62
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
63
+
64
+ # Idempotent: when the lock is already in sync nothing is pushed.
65
+ - if: ${{ steps.release_pr.outputs.branch }}
66
+ name: Sync uv.lock with the bumped version
67
+ run: |
68
+ uv lock
69
+ if git diff --quiet uv.lock; then
70
+ echo "uv.lock already in sync; nothing to push."
71
+ exit 0
72
+ fi
73
+ git config user.name "release-please[bot]"
74
+ git config user.email "release-please[bot]@users.noreply.github.com"
75
+ git commit -m "chore: sync uv.lock with release version" -- uv.lock
76
+ git push
@@ -0,0 +1,39 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ publish-pypi:
12
+ name: Publish to PyPI
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ # Trusted publishing: PyPI issues a short-lived token in exchange for a
16
+ # GitHub-signed OIDC assertion, so no API token is stored in the repo. The
17
+ # trust relationship is configured once on PyPI (pin this repo + workflow).
18
+ permissions:
19
+ id-token: write
20
+ contents: read
21
+ steps:
22
+ - name: Harden Runner
23
+ uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
24
+ with:
25
+ egress-policy: audit
26
+
27
+ - name: Checkout code
28
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
29
+ with:
30
+ persist-credentials: false
31
+
32
+ - name: Set up uv
33
+ uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
34
+
35
+ - name: Build package
36
+ run: uv build
37
+
38
+ - name: Publish to PyPI
39
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -0,0 +1,10 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+
9
+ # Secrets / local config
10
+ .env
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.0"
3
+ }
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-09-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * add create_recipe tool ([25eb043](https://github.com/cacack/mcp-server-brewfather/commit/25eb043f07651a2195c688d8d0b449f5e6fe7feb))
9
+ * add update_recipe tool for editing recipe settings and ingredients ([0241cdf](https://github.com/cacack/mcp-server-brewfather/commit/0241cdf73147b09a7e028f643ee95701820c5f9f))
10
+ * fetch only the latest reading when get_readings limit is 1 ([10d3361](https://github.com/cacack/mcp-server-brewfather/commit/10d33610e1f7e52a6015475dcb0c1e7ff87a1c26)), closes [#10](https://github.com/cacack/mcp-server-brewfather/issues/10)
11
+ * migrate to mcp 2 MCPServer ([77dd155](https://github.com/cacack/mcp-server-brewfather/commit/77dd1559f7326a1e8789906f2d459db7fcb0096c))
12
+ * scaffold Brewfather MCP server ([82b09ed](https://github.com/cacack/mcp-server-brewfather/commit/82b09ed3e4e8b161ea3e9ead97a184477438c5c7))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * harden get_readings limit=1 per panel review ([509a075](https://github.com/cacack/mcp-server-brewfather/commit/509a075fbb965602f4d8edbb3a0c5f5611628564)), closes [#10](https://github.com/cacack/mcp-server-brewfather/issues/10)
18
+ * project batch estimates and drop boolean measured flags ([c0a1e28](https://github.com/cacack/mcp-server-brewfather/commit/c0a1e2816078ef5e338c54d3a955d5471741d554))
@@ -0,0 +1,38 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ MCP server (Python, mcp 2 `MCPServer` over stdio) wrapping the Brewfather v2 REST API. User docs, tool table, and setup live in README.md.
6
+
7
+ ## Commands
8
+
9
+ ```bash
10
+ uv sync # install (incl. dev group)
11
+ uv run ruff check . && uv run ruff format --check .
12
+ uv run pytest # unit tests; acceptance auto-skipped
13
+ uv run pytest tests/unit/test_server.py::test_find_batches_rejects_unknown_status
14
+ uv run pytest --run-acceptance # live read-only API checks; needs BREWFATHER_USER_ID/API_KEY
15
+ ```
16
+
17
+ ## Architecture
18
+
19
+ Three modules in `src/mcp_server_brewfather/`, layered one direction:
20
+
21
+ - `client.py` — httpx wrapper: Basic auth from env, `start_after` pagination (`PAGE_SIZE` 50), and every failure (incl. 429) raised as `BrewfatherError` with a model-readable message. PATCH endpoints return plain text, not JSON.
22
+ - `normalize.py` — projects multi-KB API objects down to compact dicts; epoch-ms → ISO; missing/None fields omitted, never returned as null.
23
+ - `server.py` — the `@mcp.tool()` functions. Validate all input (statuses, measurement keys, inventory kinds) *before* any request is sent.
24
+
25
+ Tools must call `client.get_client()` through the module (not `from .client import get_client`): the `fake_api` fixture in `tests/conftest.py` monkeypatches that attribute. `fake_api` routes `(method, path)` to a body, a list of pages (list of lists), or an `httpx.Response`, and records requests for assertions.
26
+
27
+ ## Constraints
28
+
29
+ - No tool may call a delete endpoint. The API key's scopes are the trust boundary.
30
+ - The API accepts any field silently and never computes recipe stats (the app does). Writes go through field allowlists in `server.py`; never make stats writable.
31
+ - Recipe ingredient lists are replaced wholesale on PATCH — send full raw items, never the compact projection.
32
+ - Keep dependencies to `mcp` and `httpx`.
33
+ - Raise `ToolError` (or `BrewfatherError`, a subclass) for anything the model should read: mcp 2 replaces any other exception's message with a generic "Error executing tool".
34
+ - API is metric-only (SG, L, kg/g, °C).
35
+ - Acceptance tests must stay read-only. Detail fields added to `normalize.py` should be verified there against live objects.
36
+ - Adding/changing a tool: update the README tool table and the `server.py` module docstring.
37
+ - Commits use conventional format; PR titles must **not** (CI enforces).
38
+ - Releases: release-please (`release-please.yml`, GitHub App token) opens the release PR and syncs `uv.lock` on it; the `v*` tag it creates triggers `release.yml`, which publishes to PyPI via trusted publishing. Never bump versions or tag by hand.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Clonch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.5
2
+ Name: mcp-server-brewfather
3
+ Version: 0.1.0
4
+ Summary: MCP server for Brewfather: read batches, recipes, readings, and inventory; create and edit recipes; update batch status and stock
5
+ Project-URL: Homepage, https://github.com/cacack/mcp-server-brewfather
6
+ Project-URL: Repository, https://github.com/cacack/mcp-server-brewfather
7
+ Project-URL: Issues, https://github.com/cacack/mcp-server-brewfather/issues
8
+ Author-email: Chris Clonch <chris@theclonchs.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: brewfather,brewing,homebrew,mcp
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: httpx>=0.28
19
+ Requires-Dist: mcp[cli]<3,>=2.2
20
+ Description-Content-Type: text/markdown
21
+
22
+ # mcp-server-brewfather
23
+
24
+ An MCP server for the [Brewfather](https://brewfather.app) API. It lets an LLM read
25
+ your batches, recipes, fermentation readings, and inventory, and make the routine
26
+ writes that come up while brewing: advancing a batch's status, logging measured
27
+ gravities and volumes, tweaking a recipe, and adjusting stock after brew day.
28
+
29
+ Brewfather has no official MCP server; this wraps the public
30
+ [v2 API](https://docs.brewfather.app/api) directly.
31
+
32
+ ## Tools
33
+
34
+ | Tool | What it does |
35
+ |------|--------------|
36
+ | `find_batches(name?, status?)` | Find batches by name substring and/or status → `{id, name, batch_no, status, brewer, brew_date, recipe}` |
37
+ | `get_batch(batch_id)` | Batch summary, measured values, and embedded recipe (stats + ingredient bill) |
38
+ | `get_readings(batch_id, limit?)` | Most recent hydrometer/sensor readings, oldest→newest (`limit=0` for all; `limit=1` fetches only the latest reading, without `total`) |
39
+ | `update_batch(batch_id, status?, measurements?)` | Set status and/or `measured*` values (validated before sending) |
40
+ | `find_recipes(name?)` | Find recipes by name substring → `{id, name, author, type, style, equipment}` |
41
+ | `get_recipe(recipe_id)` | Target stats (OG, FG, ABV, IBU, color, …) and ingredient bill |
42
+ | `create_recipe(name, type?, fields?, ingredients?)` | New All Grain or Extract recipe with settings and an ingredient bill |
43
+ | `update_recipe(recipe_id, fields?, ingredients?)` | Change settings (batch size, boil time, efficiency, …) and add/change/remove ingredients |
44
+ | `list_inventory(kind, name?, in_stock_only?)` | Fermentables, hops, miscs, or yeasts in stock |
45
+ | `set_inventory(kind, item_id, amount? \| adjust?)` | Set absolute stock, or add/subtract |
46
+
47
+ All values are metric (SG, liters, kg/g, °C) — the API accepts nothing else.
48
+ Timestamps are returned as ISO-8601 UTC.
49
+
50
+ Brewfather computes recipe stats (OG, FG, ABV, IBU, color) in the app, not the API.
51
+ After `create_recipe` or `update_recipe`, the app shows correct stats as soon as you
52
+ open the recipe, but `get_recipe` returns the stored values, which the API never
53
+ calculates (a new recipe has none).
54
+ Stats can't be written through this server.
55
+
56
+ ## Setup
57
+
58
+ ### 1. Generate an API key
59
+
60
+ In Brewfather: **Settings → API → Generate API Key**. Pick scopes to match what you
61
+ want the server to do (see [Security posture](#security-posture)). Note the
62
+ **User ID** shown alongside the key.
63
+
64
+ ### 2. Install
65
+
66
+ Requires Python **3.12+**. With [uv](https://docs.astral.sh/uv/), there is nothing
67
+ to install: `uvx` fetches and runs the published package. Otherwise:
68
+
69
+ ```bash
70
+ pip install mcp-server-brewfather
71
+ # or, isolated:
72
+ pipx install mcp-server-brewfather
73
+ ```
74
+
75
+ ## Register with Claude
76
+
77
+ Claude Code:
78
+
79
+ ```bash
80
+ claude mcp add brewfather --scope user \
81
+ -e BREWFATHER_USER_ID=your_user_id -e BREWFATHER_API_KEY=your_api_key \
82
+ -- uvx mcp-server-brewfather
83
+ ```
84
+
85
+ Claude Desktop (`claude_desktop_config.json`):
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "brewfather": {
91
+ "command": "uvx",
92
+ "args": ["mcp-server-brewfather"],
93
+ "env": {
94
+ "BREWFATHER_USER_ID": "your_user_id",
95
+ "BREWFATHER_API_KEY": "your_api_key"
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ ## Security posture
103
+
104
+ - **The API key's scopes are the trust boundary.** For read-only use, grant only
105
+ `batches.read`, `recipes.read`, `inventory.read`. Add `batches.write` /
106
+ `recipes.write` / `inventory.write` to enable `update_batch` / `create_recipe` and
107
+ `update_recipe` / `set_inventory`. **Never grant `*.delete`** — no tool uses it.
108
+ - No delete tools. Every write is checked against an allowlist of fields before
109
+ it's sent, because the API silently accepts unknown fields.
110
+ - **Two dependencies only** (`mcp`, `httpx` — the latter already required by `mcp`);
111
+ pinned via the committed `uv.lock`.
112
+ - Credentials live in a gitignored `.env` / Claude config.
113
+
114
+ ## Rate limits
115
+
116
+ Brewfather allows **500 calls per hour per API key**. List tools page 50 items per
117
+ call, so `find_*`/`list_inventory` cost one call per 50 items. A rate-limited call
118
+ surfaces as an error naming the `Retry-After` delay.
119
+
120
+ ## Development
121
+
122
+ ```bash
123
+ cp .env.example .env # then fill in your user id / API key
124
+ source .env
125
+ uv sync # install deps (incl. dev group)
126
+ uv run ruff check . # lint
127
+ uv run ruff format . # format
128
+ uv run pytest # unit tests (acceptance auto-skipped)
129
+ uv run pytest --run-acceptance # + live read-only API checks (needs BREWFATHER_* creds)
130
+ ```
131
+
132
+ CI (GitHub Actions) runs the PR-title check, ruff lint/format, and the unit tests
133
+ on every PR; the `CI Success` job is the aggregate gate. Acceptance tests are not
134
+ run in CI — they need live credentials and stay local/manual. They are read-only
135
+ and never modify your brewing data.
136
+
137
+ Releases are automated: release-please keeps a release PR open from the
138
+ conventional commits on `main`, and merging it tags `vX.Y.Z`, which triggers
139
+ `release.yml` to publish to PyPI via trusted publishing.
140
+
141
+ ## License
142
+
143
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,122 @@
1
+ # mcp-server-brewfather
2
+
3
+ An MCP server for the [Brewfather](https://brewfather.app) API. It lets an LLM read
4
+ your batches, recipes, fermentation readings, and inventory, and make the routine
5
+ writes that come up while brewing: advancing a batch's status, logging measured
6
+ gravities and volumes, tweaking a recipe, and adjusting stock after brew day.
7
+
8
+ Brewfather has no official MCP server; this wraps the public
9
+ [v2 API](https://docs.brewfather.app/api) directly.
10
+
11
+ ## Tools
12
+
13
+ | Tool | What it does |
14
+ |------|--------------|
15
+ | `find_batches(name?, status?)` | Find batches by name substring and/or status → `{id, name, batch_no, status, brewer, brew_date, recipe}` |
16
+ | `get_batch(batch_id)` | Batch summary, measured values, and embedded recipe (stats + ingredient bill) |
17
+ | `get_readings(batch_id, limit?)` | Most recent hydrometer/sensor readings, oldest→newest (`limit=0` for all; `limit=1` fetches only the latest reading, without `total`) |
18
+ | `update_batch(batch_id, status?, measurements?)` | Set status and/or `measured*` values (validated before sending) |
19
+ | `find_recipes(name?)` | Find recipes by name substring → `{id, name, author, type, style, equipment}` |
20
+ | `get_recipe(recipe_id)` | Target stats (OG, FG, ABV, IBU, color, …) and ingredient bill |
21
+ | `create_recipe(name, type?, fields?, ingredients?)` | New All Grain or Extract recipe with settings and an ingredient bill |
22
+ | `update_recipe(recipe_id, fields?, ingredients?)` | Change settings (batch size, boil time, efficiency, …) and add/change/remove ingredients |
23
+ | `list_inventory(kind, name?, in_stock_only?)` | Fermentables, hops, miscs, or yeasts in stock |
24
+ | `set_inventory(kind, item_id, amount? \| adjust?)` | Set absolute stock, or add/subtract |
25
+
26
+ All values are metric (SG, liters, kg/g, °C) — the API accepts nothing else.
27
+ Timestamps are returned as ISO-8601 UTC.
28
+
29
+ Brewfather computes recipe stats (OG, FG, ABV, IBU, color) in the app, not the API.
30
+ After `create_recipe` or `update_recipe`, the app shows correct stats as soon as you
31
+ open the recipe, but `get_recipe` returns the stored values, which the API never
32
+ calculates (a new recipe has none).
33
+ Stats can't be written through this server.
34
+
35
+ ## Setup
36
+
37
+ ### 1. Generate an API key
38
+
39
+ In Brewfather: **Settings → API → Generate API Key**. Pick scopes to match what you
40
+ want the server to do (see [Security posture](#security-posture)). Note the
41
+ **User ID** shown alongside the key.
42
+
43
+ ### 2. Install
44
+
45
+ Requires Python **3.12+**. With [uv](https://docs.astral.sh/uv/), there is nothing
46
+ to install: `uvx` fetches and runs the published package. Otherwise:
47
+
48
+ ```bash
49
+ pip install mcp-server-brewfather
50
+ # or, isolated:
51
+ pipx install mcp-server-brewfather
52
+ ```
53
+
54
+ ## Register with Claude
55
+
56
+ Claude Code:
57
+
58
+ ```bash
59
+ claude mcp add brewfather --scope user \
60
+ -e BREWFATHER_USER_ID=your_user_id -e BREWFATHER_API_KEY=your_api_key \
61
+ -- uvx mcp-server-brewfather
62
+ ```
63
+
64
+ Claude Desktop (`claude_desktop_config.json`):
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "brewfather": {
70
+ "command": "uvx",
71
+ "args": ["mcp-server-brewfather"],
72
+ "env": {
73
+ "BREWFATHER_USER_ID": "your_user_id",
74
+ "BREWFATHER_API_KEY": "your_api_key"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Security posture
82
+
83
+ - **The API key's scopes are the trust boundary.** For read-only use, grant only
84
+ `batches.read`, `recipes.read`, `inventory.read`. Add `batches.write` /
85
+ `recipes.write` / `inventory.write` to enable `update_batch` / `create_recipe` and
86
+ `update_recipe` / `set_inventory`. **Never grant `*.delete`** — no tool uses it.
87
+ - No delete tools. Every write is checked against an allowlist of fields before
88
+ it's sent, because the API silently accepts unknown fields.
89
+ - **Two dependencies only** (`mcp`, `httpx` — the latter already required by `mcp`);
90
+ pinned via the committed `uv.lock`.
91
+ - Credentials live in a gitignored `.env` / Claude config.
92
+
93
+ ## Rate limits
94
+
95
+ Brewfather allows **500 calls per hour per API key**. List tools page 50 items per
96
+ call, so `find_*`/`list_inventory` cost one call per 50 items. A rate-limited call
97
+ surfaces as an error naming the `Retry-After` delay.
98
+
99
+ ## Development
100
+
101
+ ```bash
102
+ cp .env.example .env # then fill in your user id / API key
103
+ source .env
104
+ uv sync # install deps (incl. dev group)
105
+ uv run ruff check . # lint
106
+ uv run ruff format . # format
107
+ uv run pytest # unit tests (acceptance auto-skipped)
108
+ uv run pytest --run-acceptance # + live read-only API checks (needs BREWFATHER_* creds)
109
+ ```
110
+
111
+ CI (GitHub Actions) runs the PR-title check, ruff lint/format, and the unit tests
112
+ on every PR; the `CI Success` job is the aggregate gate. Acceptance tests are not
113
+ run in CI — they need live credentials and stay local/manual. They are read-only
114
+ and never modify your brewing data.
115
+
116
+ Releases are automated: release-please keeps a release PR open from the
117
+ conventional commits on `main`, and merging it tags `vX.Y.Z`, which triggers
118
+ `release.yml` to publish to PyPI via trusted publishing.
119
+
120
+ ## License
121
+
122
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "mcp-server-brewfather"
3
+ version = "0.1.0"
4
+ description = "MCP server for Brewfather: read batches, recipes, readings, and inventory; create and edit recipes; update batch status and stock"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ authors = [{ name = "Chris Clonch", email = "chris@theclonchs.com" }]
9
+ keywords = ["brewfather", "homebrew", "brewing", "mcp"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: End Users/Desktop",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ ]
17
+ dependencies = [
18
+ # 2.x: MCPServer, and only ToolError messages reach the model (server.py).
19
+ "mcp[cli]>=2.2,<3",
20
+ # Already a transitive dependency of mcp; declared because client.py uses it directly.
21
+ "httpx>=0.28",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/cacack/mcp-server-brewfather"
26
+ Repository = "https://github.com/cacack/mcp-server-brewfather"
27
+ Issues = "https://github.com/cacack/mcp-server-brewfather/issues"
28
+
29
+ [project.scripts]
30
+ mcp-server-brewfather = "mcp_server_brewfather.server:main"
31
+
32
+ [dependency-groups]
33
+ dev = ["pytest>=8.0", "ruff>=0.6"]
34
+
35
+ [build-system]
36
+ requires = ["hatchling"]
37
+ build-backend = "hatchling.build"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/mcp_server_brewfather"]
41
+
42
+ [tool.ruff]
43
+ line-length = 100
44
+ target-version = "py312"
45
+
46
+ [tool.ruff.lint]
47
+ select = ["E", "F", "I", "UP", "B"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+ markers = [
52
+ "acceptance: end-to-end tests that hit the live Brewfather API (require credentials; opt in with --run-acceptance)",
53
+ ]
54
+ addopts = "--strict-markers"