homebox-mcp 0.9.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.
- homebox_mcp-0.9.0/.env.example +21 -0
- homebox_mcp-0.9.0/.github/workflows/ci.yml +38 -0
- homebox_mcp-0.9.0/.github/workflows/publish.yml +24 -0
- homebox_mcp-0.9.0/.gitignore +32 -0
- homebox_mcp-0.9.0/CHANGELOG.md +81 -0
- homebox_mcp-0.9.0/CONTRIBUTING.md +56 -0
- homebox_mcp-0.9.0/LICENSE +21 -0
- homebox_mcp-0.9.0/PKG-INFO +341 -0
- homebox_mcp-0.9.0/README.md +294 -0
- homebox_mcp-0.9.0/annotate_location_photos.py +241 -0
- homebox_mcp-0.9.0/docs/PLAN.md +77 -0
- homebox_mcp-0.9.0/docs/REVIEW.md +151 -0
- homebox_mcp-0.9.0/heic2jpg.py +59 -0
- homebox_mcp-0.9.0/homebox_mcp.py +1701 -0
- homebox_mcp-0.9.0/pyproject.toml +51 -0
- homebox_mcp-0.9.0/server.py +20 -0
- homebox_mcp-0.9.0/tests/conftest.py +67 -0
- homebox_mcp-0.9.0/tests/test_delete.py +76 -0
- homebox_mcp-0.9.0/tests/test_locations.py +56 -0
- homebox_mcp-0.9.0/tests/test_mcp_registration.py +32 -0
- homebox_mcp-0.9.0/tests/test_pure.py +128 -0
- homebox_mcp-0.9.0/tests/test_resolve.py +92 -0
- homebox_mcp-0.9.0/tests/test_search.py +34 -0
- homebox_mcp-0.9.0/tests/test_version_and_errors.py +42 -0
- homebox_mcp-0.9.0/uv.lock +1047 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copy to .env (gitignored, chmod 600) and fill in.
|
|
2
|
+
#
|
|
3
|
+
# Create an API key in Homebox: Profile → API Keys, or
|
|
4
|
+
# POST /api/v1/users/self/api-keys {"name": "homebox-mcp"}
|
|
5
|
+
|
|
6
|
+
# Base URL of your Homebox instance (no trailing slash).
|
|
7
|
+
HOMEBOX_URL=http://localhost:7745
|
|
8
|
+
|
|
9
|
+
# Homebox API key (starts with hb_).
|
|
10
|
+
HOMEBOX_TOKEN=hb_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
11
|
+
|
|
12
|
+
# --- Optional -------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
# Name of one custom field used as a stable item identifier: tools resolve
|
|
15
|
+
# items by it and field_index() defaults to it. Unset = resolve by
|
|
16
|
+
# assetId/name only.
|
|
17
|
+
#HOMEBOX_ALIAS_FIELD=item_id
|
|
18
|
+
|
|
19
|
+
# Directory where generate_label / qrcode save PNG/JPEG output.
|
|
20
|
+
# Default: current working directory.
|
|
21
|
+
#HOMEBOX_LABEL_DIR=~/homebox-labels
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Run tests
|
|
24
|
+
# --with . installs the package (and its deps: mcp, httpx, pillow,
|
|
25
|
+
# pillow-heif) so nothing is mocked away; --with pytest/respx add the
|
|
26
|
+
# test-only tooling.
|
|
27
|
+
run: uv run --with pytest --with respx --with . pytest -q
|
|
28
|
+
|
|
29
|
+
lint:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- name: Install uv
|
|
35
|
+
uses: astral-sh/setup-uv@v5
|
|
36
|
+
|
|
37
|
+
- name: Ruff
|
|
38
|
+
run: uvx ruff check .
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
# Trusted publishing (OIDC) — no API token/secret needed.
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v5
|
|
19
|
+
|
|
20
|
+
- name: Build
|
|
21
|
+
run: uv build
|
|
22
|
+
|
|
23
|
+
- name: Publish to PyPI
|
|
24
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Secrets — never commit the live credential (keep the template)
|
|
2
|
+
.env
|
|
3
|
+
.env.local
|
|
4
|
+
!.env.example
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.pyc
|
|
9
|
+
*.pyo
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# uv
|
|
14
|
+
.uv/
|
|
15
|
+
|
|
16
|
+
# IDE / tooling
|
|
17
|
+
.claude/
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
|
|
23
|
+
# OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
|
|
27
|
+
# Local MCP configuration
|
|
28
|
+
.mcp.json
|
|
29
|
+
|
|
30
|
+
# build artifacts
|
|
31
|
+
dist/
|
|
32
|
+
*.egg-info/
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
5
|
+
versioning: [SemVer](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Changed (BREAKING)
|
|
10
|
+
- `create_item`: personal named params (`item_id`, `category`, `dossier`,
|
|
11
|
+
`resale_value`, `value_asof`, `value_source`) replaced by a generic
|
|
12
|
+
`fields: dict` — each value's JSON type picks the custom-field type
|
|
13
|
+
(string → text, number → number [integer-coerced], boolean → boolean).
|
|
14
|
+
- `set_value` replaced by generic `set_fields(identifier, fields)` (same typed
|
|
15
|
+
mapping, upsert semantics).
|
|
16
|
+
- `existing_item_ids()` replaced by `field_index(field_name?)` — a
|
|
17
|
+
`{field_value: assetId}` index over any custom field, defaulting to
|
|
18
|
+
`$HOMEBOX_ALIAS_FIELD`.
|
|
19
|
+
- Write tools (`set_warranty`, `set_fields`, `set_identity`, `set_tags`,
|
|
20
|
+
`attach_document`) now require an exact identifier (assetId, alias field, or
|
|
21
|
+
name); fuzzy matches are refused and multiple exact matches error with the
|
|
22
|
+
candidates listed, so a typo can no longer mutate the wrong item. Read tools
|
|
23
|
+
(`get_item`, `generate_label`) keep the fuzzy fallback.
|
|
24
|
+
- `set_location`'s `fields` values are typed by JSON type (was text-only).
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- `move_item(identifier, location)` — move an item via partial PATCH.
|
|
28
|
+
- `set_item(...)` — general item editor (rename, description, notes, quantity,
|
|
29
|
+
purchase info, insured/archived, custom fields); quantity-only edits use
|
|
30
|
+
PATCH.
|
|
31
|
+
- Confirm-gated delete tools: `delete_item`, `delete_location` (refuses
|
|
32
|
+
non-empty unless `confirm_nonempty=True`; sub-locations cascade, items are
|
|
33
|
+
orphaned), `delete_tag`, `delete_attachment` — `confirm` must equal the
|
|
34
|
+
target's exact name; all carry the MCP `destructiveHint` annotation.
|
|
35
|
+
- Attachment management: `list_attachments` (ids/types/titles),
|
|
36
|
+
`get_attachment` (download to a local path), `rename_attachment`
|
|
37
|
+
(title/type/primary).
|
|
38
|
+
- `search_items` accepts `tags` (tag names) and/or `query`.
|
|
39
|
+
- MCP tool annotations (`readOnlyHint`/`idempotentHint`/`destructiveHint`) on
|
|
40
|
+
all tools.
|
|
41
|
+
- `HOMEBOX_ALIAS_FIELD` env var: name one custom field as a stable item
|
|
42
|
+
identifier (items resolve by it; summaries surface it). Previously the
|
|
43
|
+
`item_id` field name was hard-coded.
|
|
44
|
+
- `HOMEBOX_LABEL_DIR` env var: where `generate_label`/`qrcode` save output
|
|
45
|
+
(default: current directory; previously a hard-coded personal path).
|
|
46
|
+
- `warranties_expiring`: `after` bound (defaults to today, so already-expired
|
|
47
|
+
warranties are excluded by default) and `lifetime=True` to list
|
|
48
|
+
lifetime-warranty items.
|
|
49
|
+
- Locations can be referenced by `/`-separated path (e.g. `Garage/Shelf 1`)
|
|
50
|
+
everywhere a location name is accepted; ambiguous bare names now error with
|
|
51
|
+
the matching paths listed instead of silently using the first match.
|
|
52
|
+
- Version guard: tools fail with a clear message against pre-0.26 Homebox.
|
|
53
|
+
- `__version__` on the server module.
|
|
54
|
+
- This changelog.
|
|
55
|
+
- Packaging: `pyproject.toml` (PyPI name `homebox-mcp`, console script
|
|
56
|
+
`homebox-mcp`); the module renamed `server.py` → `homebox_mcp.py` with a
|
|
57
|
+
`server.py` compatibility shim so existing MCP registrations keep working.
|
|
58
|
+
- Test suite (pytest + respx, fully mocked) and GitHub Actions CI
|
|
59
|
+
(tests on Python 3.10/3.13, ruff) + PyPI trusted-publishing workflow.
|
|
60
|
+
- README overhauled for public release; CONTRIBUTING.md added.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- `location_contents` never listed sub-locations on Homebox 0.26.2:
|
|
64
|
+
`GET /entities` returns non-location entities only, so location children
|
|
65
|
+
were invisible to `parentIds` queries (latent upstream-behavior bug).
|
|
66
|
+
Sub-locations now come from the entity tree; `delete_location`'s emptiness
|
|
67
|
+
check counts both kinds.
|
|
68
|
+
- `location_contents(recursive=True)` now finds items nested inside items
|
|
69
|
+
(0.26 unified entities — e.g. lenses inside a camera bag).
|
|
70
|
+
- `set_tags` uses partial PATCH instead of the GET→preserve→PUT cycle.
|
|
71
|
+
- Pagination: entity listings over one page (200 entities) were silently
|
|
72
|
+
truncated, breaking search/dedupe/warranty sweeps on large inventories.
|
|
73
|
+
- API failures now surface as legible tool errors (HTTP status + Homebox's
|
|
74
|
+
response body) instead of raw tracebacks.
|
|
75
|
+
- `create_item` date args are RFC3339-normalized like `set_warranty`'s.
|
|
76
|
+
- `create_location` duplicate check is per-parent, so the same tote/shelf name
|
|
77
|
+
can exist under different parents.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
- Default branch renamed `master` → `main`.
|
|
81
|
+
- `.env.example` documents all supported environment variables.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in improving `homebox-mcp`. It's a small, single-module
|
|
4
|
+
MCP server, so contributing is lightweight.
|
|
5
|
+
|
|
6
|
+
## Development setup
|
|
7
|
+
|
|
8
|
+
You need [`uv`](https://docs.astral.sh/uv/). The server is one module
|
|
9
|
+
(`homebox_mcp.py`) with [PEP 723](https://peps.python.org/pep-0723/) inline
|
|
10
|
+
dependencies, so there's nothing to install up front — `uv` resolves everything
|
|
11
|
+
on demand.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/dgahagan/homebox-mcp
|
|
15
|
+
cd homebox-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Tests
|
|
19
|
+
|
|
20
|
+
Tests use `pytest` with [`respx`](https://lundberg.github.io/respx/) to mock the
|
|
21
|
+
Homebox HTTP API — **no live instance is required or should be used** for the
|
|
22
|
+
suite:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv run --with pytest --with respx --with . pytest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Lint
|
|
29
|
+
|
|
30
|
+
Code is linted with [`ruff`](https://docs.astral.sh/ruff/) (config in
|
|
31
|
+
`pyproject.toml`; line length 100):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv run --with ruff ruff check .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Pull requests
|
|
38
|
+
|
|
39
|
+
- **Add tests for behavior changes.** The full-body-PUT preserve logic and the
|
|
40
|
+
typed custom-field handling are exactly the kind of thing that regresses
|
|
41
|
+
silently — cover new or changed behavior with a mocked-httpx test.
|
|
42
|
+
- **Update [`CHANGELOG.md`](CHANGELOG.md)** under the `Unreleased` section
|
|
43
|
+
([Keep a Changelog](https://keepachangelog.com/) format).
|
|
44
|
+
- **Keep it lint-clean** (`ruff check .` passes).
|
|
45
|
+
- **Keep tool docstrings accurate** — they are the source of truth for the tool
|
|
46
|
+
reference in the README and for how MCP clients present each tool.
|
|
47
|
+
|
|
48
|
+
## Testing against a live Homebox instance
|
|
49
|
+
|
|
50
|
+
If you validate a change against a real Homebox instance, **use disposable data
|
|
51
|
+
only** — create clearly-marked throwaway items/locations and delete them
|
|
52
|
+
afterward. Never test against inventory you care about, and never point the
|
|
53
|
+
suite or ad-hoc scripts at a production instance's real data.
|
|
54
|
+
|
|
55
|
+
By contributing you agree that your contributions are licensed under the
|
|
56
|
+
project's [MIT License](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dan Gahagan
|
|
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,341 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: homebox-mcp
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: MCP server for Homebox (>= 0.26): inventory Q&A, intake, attachments, labels
|
|
5
|
+
Project-URL: Homepage, https://github.com/dgahagan/homebox-mcp
|
|
6
|
+
Project-URL: Changelog, https://github.com/dgahagan/homebox-mcp/blob/main/CHANGELOG.md
|
|
7
|
+
Project-URL: Issues, https://github.com/dgahagan/homebox-mcp/issues
|
|
8
|
+
Author: Dan Gahagan
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Dan Gahagan
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: claude,homebox,inventory,mcp,model-context-protocol
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Classifier: Topic :: Home Automation
|
|
41
|
+
Requires-Python: >=3.10
|
|
42
|
+
Requires-Dist: httpx>=0.27
|
|
43
|
+
Requires-Dist: mcp>=1.10
|
|
44
|
+
Requires-Dist: pillow-heif>=0.16
|
|
45
|
+
Requires-Dist: pillow>=10
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# homebox-mcp
|
|
49
|
+
|
|
50
|
+
[](https://pypi.org/project/homebox-mcp/)
|
|
51
|
+
[](LICENSE)
|
|
52
|
+
[](https://github.com/dgahagan/homebox-mcp/actions/workflows/ci.yml)
|
|
53
|
+
|
|
54
|
+
An [MCP](https://modelcontextprotocol.io) server over the
|
|
55
|
+
[Homebox](https://homebox.software) REST API, so an MCP client like Claude can
|
|
56
|
+
work with your home inventory in plain language: *"where is my impact driver",*
|
|
57
|
+
*"what's in Tote B-3", "which warranties expire this year", "add this drill —
|
|
58
|
+
here's a photo of the receipt and the model number".* It answers questions,
|
|
59
|
+
performs intake (create an item, attach the manual/receipt, tag and file it),
|
|
60
|
+
manages attachments, and prints QR labels for totes and shelves — no
|
|
61
|
+
hand-rolled `curl`.
|
|
62
|
+
|
|
63
|
+
## Quickstart
|
|
64
|
+
|
|
65
|
+
You need a running Homebox instance (**0.26+**, see [Requirements](#requirements))
|
|
66
|
+
and an API key from **Profile → API Keys** in the Homebox web UI.
|
|
67
|
+
|
|
68
|
+
### Claude Code
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
claude mcp add homebox --scope user \
|
|
72
|
+
-e HOMEBOX_URL=https://homebox.example.com \
|
|
73
|
+
-e HOMEBOX_TOKEN=hb_xxxxxxxxxxxxxxxxxxxxxxxx \
|
|
74
|
+
-- uvx homebox-mcp
|
|
75
|
+
claude mcp get homebox # should show: Status ✔ Connected
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`--scope user` makes it available from any project. `uvx` fetches and runs the
|
|
79
|
+
published package in an ephemeral environment — nothing to install first.
|
|
80
|
+
|
|
81
|
+
### Claude Desktop / generic MCP clients
|
|
82
|
+
|
|
83
|
+
Add to your client's MCP config (e.g. `claude_desktop_config.json`):
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"homebox": {
|
|
89
|
+
"command": "uvx",
|
|
90
|
+
"args": ["homebox-mcp"],
|
|
91
|
+
"env": {
|
|
92
|
+
"HOMEBOX_URL": "https://homebox.example.com",
|
|
93
|
+
"HOMEBOX_TOKEN": "hb_xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### From a clone (no PyPI)
|
|
101
|
+
|
|
102
|
+
The module carries [PEP 723](https://peps.python.org/pep-0723/) inline
|
|
103
|
+
dependencies, so it also runs standalone with `uv run --script` — `uv` resolves
|
|
104
|
+
`mcp`, `httpx`, `pillow`, and `pillow-heif` into an ephemeral venv on first run.
|
|
105
|
+
Instead of env vars, drop credentials in a `.env` file next to the module:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git clone https://github.com/dgahagan/homebox-mcp
|
|
109
|
+
cd homebox-mcp
|
|
110
|
+
cp .env.example .env # .env is gitignored
|
|
111
|
+
chmod 600 .env
|
|
112
|
+
# edit .env: HOMEBOX_URL + HOMEBOX_TOKEN
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Then register the **absolute path** to the module (or to `server.py`, a
|
|
116
|
+
compatibility shim kept for older registrations):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
claude mcp add homebox --scope user -- \
|
|
120
|
+
uv run --script /path/to/homebox-mcp/homebox_mcp.py
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The server reads config from the environment, falling back to the sibling
|
|
124
|
+
`.env` (resolved relative to the script, so the absolute-path invocation still
|
|
125
|
+
finds it). The credential never enters the MCP config — it stays in the
|
|
126
|
+
gitignored `.env`.
|
|
127
|
+
|
|
128
|
+
## Requirements
|
|
129
|
+
|
|
130
|
+
- **Homebox 0.26 or newer** — the actively maintained
|
|
131
|
+
[sysadminsmedia fork](https://github.com/sysadminsmedia/homebox) at
|
|
132
|
+
[homebox.software](https://homebox.software). This server speaks the unified
|
|
133
|
+
**entities** API introduced in 0.26. Older instances use a different API
|
|
134
|
+
(`/items`, `/locations`, `/labels`) and are rejected on the first tool call
|
|
135
|
+
with a clear error rather than failing cryptically.
|
|
136
|
+
- **An API key** — Homebox **Profile → API Keys** (the token starts with `hb_`).
|
|
137
|
+
- **[`uv`](https://docs.astral.sh/uv/)**, or any **Python ≥ 3.10** environment
|
|
138
|
+
where you install `homebox-mcp` yourself.
|
|
139
|
+
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
All configuration is via environment variables (or the sibling `.env` for the
|
|
143
|
+
clone workflow):
|
|
144
|
+
|
|
145
|
+
| Variable | Required | Purpose |
|
|
146
|
+
|----------|----------|---------|
|
|
147
|
+
| `HOMEBOX_URL` | yes | Base URL of your Homebox instance (no trailing slash). |
|
|
148
|
+
| `HOMEBOX_TOKEN` | yes | Homebox API key (`hb_…`). |
|
|
149
|
+
| `HOMEBOX_ALIAS_FIELD` | no | Name of one custom field to treat as a stable item identifier — items can be resolved by it, summaries surface it, and `field_index` defaults to it. Unset = resolve by assetId/name only. See [Conventions](#conventions-optional). |
|
|
150
|
+
| `HOMEBOX_LABEL_DIR` | no | Where `generate_label` / `qrcode` save output. Default: current working directory. |
|
|
151
|
+
|
|
152
|
+
## Tools
|
|
153
|
+
|
|
154
|
+
~33 tools. Reads accept a **fuzzy** identifier (assetId, alias field, exact
|
|
155
|
+
name, then first keyword match). Write tools require an **exact** identifier
|
|
156
|
+
(assetId, alias field, or exact name) — a typo or an ambiguous match is refused
|
|
157
|
+
rather than mutating the wrong item. Locations are referenced by name or
|
|
158
|
+
`/`-separated path (e.g. `Garage/Shelf 1`) to disambiguate duplicate names.
|
|
159
|
+
|
|
160
|
+
### Read / Q&A
|
|
161
|
+
|
|
162
|
+
| Tool | Purpose |
|
|
163
|
+
|------|---------|
|
|
164
|
+
| `search_items(query?, tags?, limit=20)` | Search items by keyword and/or tag names (AND of both); returns each with assetId, location, and the alias field. |
|
|
165
|
+
| `get_item(identifier)` | Full detail for one item: location path, identity, purchase, warranty, custom fields, tags, attachments. |
|
|
166
|
+
| `list_locations()` | The full location tree as an indented outline. |
|
|
167
|
+
| `location_contents(location, recursive=False)` | Items directly in a location plus its sub-location names; `recursive=True` walks the whole subtree and returns every nested item with its full location path. |
|
|
168
|
+
| `list_tags(detail=False)` | All tag (label) names; `detail=True` returns full objects (description, color, icon, parent tag). |
|
|
169
|
+
| `warranties_expiring(before?, after?, lifetime=False)` | Items whose warranty expires in `after…before` (`after` defaults to today, excluding already-expired); `lifetime=True` lists lifetime-warranty items. |
|
|
170
|
+
|
|
171
|
+
### Create & intake
|
|
172
|
+
|
|
173
|
+
| Tool | Purpose |
|
|
174
|
+
|------|---------|
|
|
175
|
+
| `create_item(name, location?, quantity=1, manufacturer?, model?, serial?, purchase_price?, purchase_date?, purchase_from?, warranty_expires?, notes?, fields?, tags?)` | Create and enrich an item in one call. `fields` is a dict typed by JSON value (string→text, number→number [integer-coerced], bool→boolean); `tags` must already exist. Returns the new assetId. |
|
|
176
|
+
| `import_csv(csv_text)` | Bulk-create items and locations from a Homebox CSV in one multipart request. `HB.location` **auto-creates** the path hierarchy; recognizes `HB.name`, `HB.tags`, `HB.quantity`, `HB.serial_number`, `HB.model_number`, `HB.manufacturer`, `HB.notes`, `HB.purchase_*`, `HB.warranty_expires`, `HB.field.<name>`. |
|
|
177
|
+
| `create_location(name, parent?, description?)` | Create a location (tote/bin/shelf) to bootstrap a new storage spot; `description` doubles as a contents manifest. |
|
|
178
|
+
| `barcode_lookup(code)` | UPC/EAN → name/manufacturer/model (optional, for boxed goods). |
|
|
179
|
+
|
|
180
|
+
### Edit
|
|
181
|
+
|
|
182
|
+
All write tools resolve by exact identifier and preserve everything you don't
|
|
183
|
+
touch (a full-body PUT that echoes the rest of the item back — see
|
|
184
|
+
[gotchas](#how-it-works-and-homebox-026-gotchas)).
|
|
185
|
+
|
|
186
|
+
| Tool | Purpose |
|
|
187
|
+
|------|---------|
|
|
188
|
+
| `set_item(identifier, new_name?, description?, notes?, quantity?, purchase_price?, purchase_date?, purchase_from?, insured?, archived?, fields?)` | General item editor: rename, notes, quantity, purchase info, insured/archived flags, custom fields. Quantity-only edits use a partial PATCH. |
|
|
189
|
+
| `move_item(identifier, location)` | Move an item to another location (partial PATCH — nothing else changes). |
|
|
190
|
+
| `set_warranty(identifier, expires?, lifetime?, details?)` | Set warranty end date, lifetime flag, and terms summary. |
|
|
191
|
+
| `set_identity(identifier, manufacturer?, model_number?, serial_number?)` | Set manufacturer / model / serial (e.g. after a nameplate photo reveals them). |
|
|
192
|
+
| `set_fields(identifier, fields)` | Create or overwrite custom fields (upsert; typed by JSON value type). |
|
|
193
|
+
| `set_tags(identifier, tags, mode="add")` | Add / remove / replace tags on an item; unknown tag names are auto-created (partial PATCH). |
|
|
194
|
+
| `set_tag(name, new_name?, description?, color?, icon?, parent?, clear_parent=False)` | Edit **a tag's own** metadata (rename, color, icon, parent tag for grouping) — not what's tagged on an item. Creates the tag if new. |
|
|
195
|
+
| `set_location(location, new_name?, parent?, clear_parent?, description?, notes?, tags?, tags_mode?, entity_type?, asset_id?, fields?)` | General location editor: rename, move (or `clear_parent` to root), tags, notes, entity type, assetId, custom fields. |
|
|
196
|
+
| `set_location_manifest(location, description)` | Set a location's contents-manifest `description` (echoes name/parent/assetId so the PUT doesn't wipe them). |
|
|
197
|
+
|
|
198
|
+
### Attachments
|
|
199
|
+
|
|
200
|
+
| Tool | Purpose |
|
|
201
|
+
|------|---------|
|
|
202
|
+
| `attach_document(identifier, source, title, doc_type="manual", primary=False)` | Attach a file (local **path or http(s) URL**) to an item, or to a location by name as a fallback. `doc_type` e.g. manual/receipt/warranty/photo. |
|
|
203
|
+
| `attach_location_photo(location, source, title?, primary=True)` | Attach a photo to a **location** as its primary "this is the spot" image for wayfinding. |
|
|
204
|
+
| `list_attachments(identifier)` | List an item's or location's attachments **with their ids** (the handle the other attachment tools need). |
|
|
205
|
+
| `get_attachment(identifier, attachment_id, save_to)` | Download one attachment to a local path so its content (e.g. a manual) can be read. |
|
|
206
|
+
| `rename_attachment(identifier, attachment_id, title?, doc_type?, primary?)` | Update an attachment's title, type, or primary flag. |
|
|
207
|
+
|
|
208
|
+
> Uploads auto-convert HEIC/HEIF (iPhone photos) to JPEG so Homebox always
|
|
209
|
+
> stores a browser-renderable image, and sanitize `/` in titles (Homebox treats
|
|
210
|
+
> a title as a path and would otherwise truncate it).
|
|
211
|
+
|
|
212
|
+
### Deletes (confirm-gated)
|
|
213
|
+
|
|
214
|
+
Every delete requires `confirm` to equal the target's exact name and carries
|
|
215
|
+
the MCP `destructiveHint` annotation, so clients prompt before running them.
|
|
216
|
+
|
|
217
|
+
| Tool | Purpose |
|
|
218
|
+
|------|---------|
|
|
219
|
+
| `delete_item(identifier, confirm)` | Permanently delete an item and its attachments. No undo. |
|
|
220
|
+
| `delete_location(location, confirm, confirm_nonempty=False)` | Delete a location. A non-empty one is refused unless `confirm_nonempty=True`: **sub-locations cascade (are deleted); items are orphaned to the top level** (not deleted). |
|
|
221
|
+
| `delete_tag(name, confirm)` | Delete a tag itself (removed from every tagged item; the items survive). |
|
|
222
|
+
| `delete_attachment(identifier, attachment_id, confirm)` | Delete one attachment (`confirm` = its exact title). |
|
|
223
|
+
|
|
224
|
+
### Labels & finalize
|
|
225
|
+
|
|
226
|
+
| Tool | Purpose |
|
|
227
|
+
|------|---------|
|
|
228
|
+
| `generate_label(identifier, kind="location", out_dir?)` | Save a printable label **PNG** (QR + readable name) for a `location`, `item`, or `asset`. Saves to `$HOMEBOX_LABEL_DIR` (else CWD). |
|
|
229
|
+
| `qrcode(data, out_dir?)` | Save a raw QR **JPEG** for arbitrary data (prefer `generate_label` for totes — it adds the name). |
|
|
230
|
+
| `set_primary_photos()` | Ensure every entity with photos has a primary image (finalize after a bulk photo attach). |
|
|
231
|
+
| `create_thumbnails()` | Generate any missing photo thumbnails (finalize after a bulk photo attach). |
|
|
232
|
+
| `field_index(field_name?)` | `{field_value: assetId}` index over a custom field (default `$HOMEBOX_ALIAS_FIELD`) — a one-pass **dedupe** index before a bulk import, since `q` doesn't index custom fields. |
|
|
233
|
+
|
|
234
|
+
## How it works (and Homebox 0.26 gotchas)
|
|
235
|
+
|
|
236
|
+
The value of this server is as much in what it works *around* as in what it
|
|
237
|
+
exposes. These are hard-won behaviors of the 0.26 entities API:
|
|
238
|
+
|
|
239
|
+
- **Items and locations are unified "entities"** (`/v1/entities`); a location is
|
|
240
|
+
an entity whose entity-type has `isLocation=true`. A new item with no
|
|
241
|
+
entityTypeId lazily gets a default `Item` type.
|
|
242
|
+
- **`GET /entities` returns non-location entities only.** Location children never
|
|
243
|
+
appear in `/entities` results, even with `parentIds` — sub-locations come from
|
|
244
|
+
the entity tree (`/entities/tree`). `location_contents` merges both sources, and
|
|
245
|
+
`delete_location`'s emptiness check counts items *and* sub-locations.
|
|
246
|
+
- **Deleting a non-empty location cascades sub-locations but orphans items.**
|
|
247
|
+
Force-deleting a location (`confirm_nonempty=True`) deletes its sub-locations
|
|
248
|
+
with it, but its **items survive**, re-parented to the top level. Move items
|
|
249
|
+
out first if you don't want them loose.
|
|
250
|
+
- **List/search responses are lightweight summaries** — they omit `fields` and
|
|
251
|
+
have an empty `assetId`. The server fetches full detail (`GET /entities/{id}`)
|
|
252
|
+
before returning assetId/alias, so `search_items`/`get_item` are accurate.
|
|
253
|
+
- **`q` does not index custom fields.** Resolving by an alias-field value uses a
|
|
254
|
+
scan fallback (one detail fetch per item). Resolving by **assetId** (via
|
|
255
|
+
`/v1/assets/{id}`) or **name** is cheaper — prefer assetId when you have it
|
|
256
|
+
(e.g. the value `create_item` returns).
|
|
257
|
+
- **PUT clears omitted fields**, including `assetId`. Every write tool rebuilds a
|
|
258
|
+
full body from a fresh GET (`_preserve_item_body`) and overrides only the
|
|
259
|
+
targeted keys. `create_item` assigns the asset ID *after* enrichment so it
|
|
260
|
+
survives.
|
|
261
|
+
- **Attachment titles are basenamed on `/`.** Homebox treats a title as
|
|
262
|
+
path-like, so "front 3/4 view" silently stores as "4 view". The server
|
|
263
|
+
sanitizes `/`→`-` in attach titles.
|
|
264
|
+
- **Number custom fields are integer-typed.** A float `numberValue` (e.g.
|
|
265
|
+
`17000.0`, which FastMCP produces from a `float`-typed arg) makes the API
|
|
266
|
+
**500 "Unknown Error"**. The server coerces number values to `int`; pass whole
|
|
267
|
+
numbers.
|
|
268
|
+
- **Custom-field values are type-keyed** (`textValue` / `numberValue` /
|
|
269
|
+
`booleanValue`). The server rebuilds each field by its declared type —
|
|
270
|
+
coercing everything to `textValue` (an earlier bug) silently wiped
|
|
271
|
+
numeric/boolean fields on any PUT.
|
|
272
|
+
- **Pagination.** Entity listings over one page (200) were silently truncated;
|
|
273
|
+
the server pages through to the reported `total`, so search, dedupe, and
|
|
274
|
+
warranty sweeps stay correct on large inventories.
|
|
275
|
+
|
|
276
|
+
## Conventions (optional)
|
|
277
|
+
|
|
278
|
+
Homebox's built-in `assetId` is a stable numeric handle, but it isn't something
|
|
279
|
+
you'd type from memory. If you want a **human-meaningful stable identifier** —
|
|
280
|
+
useful for cross-referencing items with receipts, spreadsheets, or another
|
|
281
|
+
system — add a custom field (say `item_id`) holding a slug like
|
|
282
|
+
`makita-xdt131` and point `HOMEBOX_ALIAS_FIELD` at it. Then:
|
|
283
|
+
|
|
284
|
+
- items resolve by that slug in every read and write tool,
|
|
285
|
+
- `search_items`/`get_item` summaries surface it, and
|
|
286
|
+
- `field_index()` defaults to it, giving you a one-call dedupe map before a bulk
|
|
287
|
+
import.
|
|
288
|
+
|
|
289
|
+
This is entirely optional — leave `HOMEBOX_ALIAS_FIELD` unset and items resolve
|
|
290
|
+
by assetId or name only. Nothing about the field name is special; any single
|
|
291
|
+
custom field works.
|
|
292
|
+
|
|
293
|
+
## Companion scripts
|
|
294
|
+
|
|
295
|
+
Two standalone helper scripts ship in the repo (not MCP tools):
|
|
296
|
+
|
|
297
|
+
- **`annotate_location_photos.py`** turns one wide photo of a shelf bank into one
|
|
298
|
+
boxed/labeled image per shelf (`--rows N`), and can upload each straight to its
|
|
299
|
+
Homebox location with `--upload`. See its `--help`.
|
|
300
|
+
- **`heic2jpg.py`** converts HEIC/HEIF (iPhone photos) to JPEG for ad-hoc use
|
|
301
|
+
(`./heic2jpg.py FILE...`). Note the attach tools **already auto-convert HEIC on
|
|
302
|
+
upload**, so this is only needed to feed HEIC frames to a vision step *before*
|
|
303
|
+
upload.
|
|
304
|
+
|
|
305
|
+
Both rely on `pillow-heif`, which bundles libheif — there's no system package to
|
|
306
|
+
install.
|
|
307
|
+
|
|
308
|
+
## Security
|
|
309
|
+
|
|
310
|
+
- **API key handling.** In the clone workflow the key lives in `.env`, which is
|
|
311
|
+
gitignored and should be mode `600` (`chmod 600 .env`). Never commit it. In the
|
|
312
|
+
`uvx` workflow it's passed through your MCP client's config — treat that file
|
|
313
|
+
as a secret too.
|
|
314
|
+
- **Key rotation.** Rotating the Homebox `HBOX_AUTH_API_KEY_PEPPER` invalidates
|
|
315
|
+
every issued key — mint a new one and update your config.
|
|
316
|
+
- **Confirm-gated deletes.** All four delete tools require `confirm` to equal the
|
|
317
|
+
target's exact name and carry the MCP `destructiveHint` annotation, so a
|
|
318
|
+
well-behaved client prompts before any destructive call.
|
|
319
|
+
- **Use HTTPS** for any remote Homebox instance — the API key is sent on every
|
|
320
|
+
request in the `Authorization` header.
|
|
321
|
+
|
|
322
|
+
## Development
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
git clone https://github.com/dgahagan/homebox-mcp
|
|
326
|
+
cd homebox-mcp
|
|
327
|
+
|
|
328
|
+
# Run the test suite (httpx mocked with respx — no live instance needed)
|
|
329
|
+
uv run --with pytest --with respx --with . pytest
|
|
330
|
+
|
|
331
|
+
# Lint
|
|
332
|
+
uv run --with ruff ruff check .
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The server is a single module (`homebox_mcp.py`) with PEP 723 inline
|
|
336
|
+
dependencies. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and PR
|
|
337
|
+
expectations, and [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
338
|
+
|
|
339
|
+
## License
|
|
340
|
+
|
|
341
|
+
[MIT](LICENSE) © Dan Gahagan
|