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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/homebox-mcp.svg)](https://pypi.org/project/homebox-mcp/)
51
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
52
+ [![CI](https://github.com/dgahagan/homebox-mcp/actions/workflows/ci.yml/badge.svg)](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