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.
- mcp_server_brewfather-0.1.0/.env.example +4 -0
- mcp_server_brewfather-0.1.0/.github/dependabot.yml +14 -0
- mcp_server_brewfather-0.1.0/.github/workflows/ci.yml +119 -0
- mcp_server_brewfather-0.1.0/.github/workflows/release-please.yml +76 -0
- mcp_server_brewfather-0.1.0/.github/workflows/release.yml +39 -0
- mcp_server_brewfather-0.1.0/.gitignore +10 -0
- mcp_server_brewfather-0.1.0/.python-version +1 -0
- mcp_server_brewfather-0.1.0/.release-please-manifest.json +3 -0
- mcp_server_brewfather-0.1.0/CHANGELOG.md +18 -0
- mcp_server_brewfather-0.1.0/CLAUDE.md +38 -0
- mcp_server_brewfather-0.1.0/LICENSE +21 -0
- mcp_server_brewfather-0.1.0/PKG-INFO +143 -0
- mcp_server_brewfather-0.1.0/README.md +122 -0
- mcp_server_brewfather-0.1.0/pyproject.toml +54 -0
- mcp_server_brewfather-0.1.0/release-please-config.json +25 -0
- mcp_server_brewfather-0.1.0/src/mcp_server_brewfather/__init__.py +3 -0
- mcp_server_brewfather-0.1.0/src/mcp_server_brewfather/client.py +94 -0
- mcp_server_brewfather-0.1.0/src/mcp_server_brewfather/normalize.py +136 -0
- mcp_server_brewfather-0.1.0/src/mcp_server_brewfather/server.py +352 -0
- mcp_server_brewfather-0.1.0/tests/acceptance/test_read_only.py +49 -0
- mcp_server_brewfather-0.1.0/tests/conftest.py +104 -0
- mcp_server_brewfather-0.1.0/tests/unit/test_client.py +71 -0
- mcp_server_brewfather-0.1.0/tests/unit/test_server.py +336 -0
- mcp_server_brewfather-0.1.0/uv.lock +870 -0
|
@@ -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 @@
|
|
|
1
|
+
3.12
|
|
@@ -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"
|