{
  "markdown": "# homebox-mcp\n\n<!-- mcp-name: io.github.dgahagan/homebox-mcp -->\n\n[![PyPI version](https://img.shields.io/pypi/v/homebox-mcp)](https://pypi.org/project/homebox-mcp/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![CI](https://github.com/dgahagan/homebox-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/dgahagan/homebox-mcp/actions/workflows/ci.yml)\n\nAn [MCP](https://modelcontextprotocol.io) server over the\n[Homebox](https://homebox.software) REST API, so an MCP client like Claude can\nwork with your home inventory in plain language: *\"where is my impact driver\",*\n*\"what's in Tote B-3\", \"which warranties expire this year\", \"add this drill —\nhere's a photo of the receipt and the model number\".* It answers questions,\nperforms intake (create an item, attach the manual/receipt, tag and file it),\nmanages attachments, and prints QR labels for totes and shelves — no\nhand-rolled `curl`.\n\n## Quickstart\n\nYou need a running Homebox instance (**0.26+**, see [Requirements](#requirements))\nand an API key from **Profile → API Keys** in the Homebox web UI.\n\n### Claude Code\n\n```bash\nclaude mcp add homebox --scope user \\\n  -e HOMEBOX_URL=https://homebox.example.com \\\n  -e HOMEBOX_TOKEN=hb_xxxxxxxxxxxxxxxxxxxxxxxx \\\n  -- uvx homebox-mcp\nclaude mcp get homebox      # should show: Status ✔ Connected\n```\n\n`--scope user` makes it available from any project. `uvx` fetches and runs the\npublished package in an ephemeral environment — nothing to install first.\n\n### Claude Desktop / generic MCP clients\n\nAdd to your client's MCP config (e.g. `claude_desktop_config.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"homebox\": {\n      \"command\": \"uvx\",\n      \"args\": [\"homebox-mcp\"],\n      \"env\": {\n        \"HOMEBOX_URL\": \"https://homebox.example.com\",\n        \"HOMEBOX_TOKEN\": \"hb_xxxxxxxxxxxxxxxxxxxxxxxx\"\n      }\n    }\n  }\n}\n```\n\n### From a clone (no PyPI)\n\nThe module carries [PEP 723](https://peps.python.org/pep-0723/) inline\ndependencies, so it also runs standalone with `uv run --script` — `uv` resolves\n`mcp`, `httpx`, `pillow`, and `pillow-heif` into an ephemeral venv on first run.\nInstead of env vars, drop credentials in a `.env` file next to the module:\n\n```bash\ngit clone https://github.com/dgahagan/homebox-mcp\ncd homebox-mcp\ncp .env.example .env        # .env is gitignored\nchmod 600 .env\n# edit .env: HOMEBOX_URL + HOMEBOX_TOKEN\n```\n\nThen register the **absolute path** to the module (or to `server.py`, a\ncompatibility shim kept for older registrations):\n\n```bash\nclaude mcp add homebox --scope user -- \\\n  uv run --script /path/to/homebox-mcp/homebox_mcp.py\n```\n\nThe server reads config from the environment, falling back to the sibling\n`.env` (resolved relative to the script, so the absolute-path invocation still\nfinds it). The credential never enters the MCP config — it stays in the\ngitignored `.env`.\n\n## Requirements\n\n- **Homebox 0.26 or newer** — the actively maintained\n  [sysadminsmedia fork](https://github.com/sysadminsmedia/homebox) at\n  [homebox.software](https://homebox.software). This server speaks the unified\n  **entities** API introduced in 0.26. Older instances use a different API\n  (`/items`, `/locations`, `/labels`) and are rejected on the first tool call\n  with a clear error rather than failing cryptically.\n- **An API key** — Homebox **Profile → API Keys** (the token starts with `hb_`).\n- **[`uv`](https://docs.astral.sh/uv/)**, or any **Python ≥ 3.10** environment\n  where you install `homebox-mcp` yourself.\n\n## Configuration\n\nAll configuration is via environment variables (or the sibling `.env` for the\nclone workflow):\n\n| Variable | Required | Purpose |\n|----------|----------|---------|\n| `HOMEBOX_URL` | yes | Base URL of your Homebox instance (no trailing slash). |\n| `HOMEBOX_TOKEN` | yes | Homebox API key (`hb_…`). |\n| `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). |\n| `HOMEBOX_LABEL_DIR` | no | Where `generate_label` / `qrcode` save output. Default: current working directory. |\n\n## Tools\n\n~39 tools. Reads accept a **fuzzy** identifier (assetId, alias field, exact\nname, then first keyword match). Write tools require an **exact** identifier\n(assetId, alias field, or exact name) — a typo or an ambiguous match is refused\nrather than mutating the wrong item. Locations are referenced by name or\n`/`-separated path (e.g. `Garage/Shelf 1`) to disambiguate duplicate names.\n\n### Read / Q&A\n\n| Tool | Purpose |\n|------|---------|\n| `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. |\n| `get_item(identifier)` | Full detail for one item: location path, identity, purchase, warranty, custom fields, tags, attachments. |\n| `list_locations()` | The full location tree as an indented outline. |\n| `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. |\n| `list_tags(detail=False)` | All tag (label) names; `detail=True` returns full objects (description, color, icon, parent tag). |\n| `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. |\n\n### Create & intake\n\n| Tool | Purpose |\n|------|---------|\n| `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. |\n| `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>`. |\n| `create_location(name, parent?, description?)` | Create a location (tote/bin/shelf) to bootstrap a new storage spot; `description` doubles as a contents manifest. |\n| `barcode_lookup(code)` | UPC/EAN → name/manufacturer/model (optional, for boxed goods). |\n| `duplicate_item(identifier, copy_attachments=False, copy_custom_fields=True, copy_maintenance=False, prefix=\"Copy of \")` | Duplicate an item (\"I bought a second one\"). Copied custom fields include the alias field verbatim — give the copy its own value after. |\n\n### Edit\n\nAll write tools resolve by exact identifier and preserve everything you don't\ntouch (a full-body PUT that echoes the rest of the item back — see\n[gotchas](#how-it-works-and-homebox-026-gotchas)).\n\n| Tool | Purpose |\n|------|---------|\n| `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. |\n| `move_item(identifier, location)` | Move an item to another location (partial PATCH — nothing else changes). |\n| `set_warranty(identifier, expires?, lifetime?, details?)` | Set warranty end date, lifetime flag, and terms summary. |\n| `set_identity(identifier, manufacturer?, model_number?, serial_number?)` | Set manufacturer / model / serial (e.g. after a nameplate photo reveals them). |\n| `set_fields(identifier, fields)` | Create or overwrite custom fields (upsert; typed by JSON value type). |\n| `set_tags(identifier, tags, mode=\"add\")` | Add / remove / replace tags on an item; unknown tag names are auto-created (partial PATCH). |\n| `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. |\n| `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. |\n| `mark_sold(identifier, sold_price?, sold_to?, sold_date?, sold_notes?, clear=False)` | Record a sale (price/buyer/date/notes) or `clear=True` to un-sell; pair with `set_item(archived=True)` to retire the item. |\n\n### Maintenance log\n\n| Tool | Purpose |\n|------|---------|\n| `log_maintenance(identifier, name, description?, completed_date?, scheduled_date?, cost?)` | Add an entry — \"changed the mower oil today\" (completed) or \"sharpen blades in spring\" (scheduled). |\n| `list_maintenance(identifier?, status=\"both\")` | Entries for one item, or across the whole inventory (\"what maintenance is due?\"); `status` = scheduled / completed / both. |\n| `set_maintenance(entry_id, ...)` | Edit an entry — e.g. mark a scheduled one completed by setting `completed_date`. |\n| `delete_maintenance(entry_id, confirm)` | Delete one entry (`confirm` = its exact name). |\n\n### Reporting\n\n| Tool | Purpose |\n|------|---------|\n| `inventory_stats(by=\"totals\", start?, end?)` | Totals (counts, total value, warranty count), value by location or tag, or purchase-price over time — the cheap way to answer \"what's my inventory worth?\". |\n| `export_csv(save_to?)` | Export the whole inventory as a Homebox CSV (complement of `import_csv`; quick backup). |\n| `list_custom_fields(field?)` | Discover the custom-field schema in use: all field names, or every distinct value of one field. |\n\n### Attachments\n\n| Tool | Purpose |\n|------|---------|\n| `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; `primary=True` makes a photo the entity's primary image (e.g. a location's \"this is the spot\" wayfinding shot). |\n| `list_attachments(identifier)` | List an item's or location's attachments **with their ids** (the handle the other attachment tools need). |\n| `get_attachment(identifier, attachment_id, save_to)` | Download one attachment to a local path so its content (e.g. a manual) can be read. |\n| `rename_attachment(identifier, attachment_id, title?, doc_type?, primary?)` | Update an attachment's title, type, or primary flag. |\n\n> Uploads auto-convert HEIC/HEIF (iPhone photos) to JPEG so Homebox always\n> stores a browser-renderable image, and sanitize `/` in titles (Homebox treats\n> a title as a path and would otherwise truncate it).\n\n### Deletes (confirm-gated)\n\nEvery delete requires `confirm` to equal the target's exact name and carries\nthe MCP `destructiveHint` annotation, so clients prompt before running them.\n\n| Tool | Purpose |\n|------|---------|\n| `delete_item(identifier, confirm)` | Permanently delete an item and its attachments. No undo. |\n| `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). |\n| `delete_tag(name, confirm)` | Delete a tag itself (removed from every tagged item; the items survive). |\n| `delete_attachment(identifier, attachment_id, confirm)` | Delete one attachment (`confirm` = its exact title). |\n\n### Labels & finalize\n\n| Tool | Purpose |\n|------|---------|\n| `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). |\n| `qrcode(data, out_dir?)` | Save a raw QR **JPEG** for arbitrary data (prefer `generate_label` for totes — it adds the name). |\n| `finalize_photos()` | Finalize after a bulk photo attach: set missing primary images, then generate missing thumbnails. |\n| `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. |\n\n## How it works (and Homebox 0.26 gotchas)\n\nThe value of this server is as much in what it works *around* as in what it\nexposes. These are hard-won behaviors of the 0.26 entities API:\n\n- **Items and locations are unified \"entities\"** (`/v1/entities`); a location is\n  an entity whose entity-type has `isLocation=true`. A new item with no\n  entityTypeId lazily gets a default `Item` type.\n- **`GET /entities` returns non-location entities only.** Location children never\n  appear in `/entities` results, even with `parentIds` — sub-locations come from\n  the entity tree (`/entities/tree`). `location_contents` merges both sources, and\n  `delete_location`'s emptiness check counts items *and* sub-locations.\n- **Deleting a non-empty location cascades sub-locations but orphans items.**\n  Force-deleting a location (`confirm_nonempty=True`) deletes its sub-locations\n  with it, but its **items survive**, re-parented to the top level. Move items\n  out first if you don't want them loose.\n- **List/search responses are lightweight summaries** — they omit `fields` and\n  have an empty `assetId`. The server fetches full detail (`GET /entities/{id}`)\n  before returning assetId/alias, so `search_items`/`get_item` are accurate.\n- **`q` does not index custom fields.** Resolving by an alias-field value uses a\n  scan fallback (one detail fetch per item). Resolving by **assetId** (via\n  `/v1/assets/{id}`) or **name** is cheaper — prefer assetId when you have it\n  (e.g. the value `create_item` returns).\n- **PUT clears omitted fields**, including `assetId`. Every write tool rebuilds a\n  full body from a fresh GET (`_preserve_item_body`) and overrides only the\n  targeted keys. `create_item` assigns the asset ID *after* enrichment so it\n  survives.\n- **Attachment titles are basenamed on `/`.** Homebox treats a title as\n  path-like, so \"front 3/4 view\" silently stores as \"4 view\". The server\n  sanitizes `/`→`-` in attach titles.\n- **Number custom fields are integer-typed.** A float `numberValue` (e.g.\n  `17000.0`, which FastMCP produces from a `float`-typed arg) makes the API\n  **500 \"Unknown Error\"**. The server coerces number values to `int`; pass whole\n  numbers.\n- **Custom-field values are type-keyed** (`textValue` / `numberValue` /\n  `booleanValue`). The server rebuilds each field by its declared type —\n  coercing everything to `textValue` (an earlier bug) silently wiped\n  numeric/boolean fields on any PUT.\n- **Pagination.** Entity listings over one page (200) were silently truncated;\n  the server pages through to the reported `total`, so search, dedupe, and\n  warranty sweeps stay correct on large inventories.\n\n## Conventions (optional)\n\nHomebox's built-in `assetId` is a stable numeric handle, but it isn't something\nyou'd type from memory. If you want a **human-meaningful stable identifier** —\nuseful for cross-referencing items with receipts, spreadsheets, or another\nsystem — add a custom field (say `item_id`) holding a slug like\n`makita-xdt131` and point `HOMEBOX_ALIAS_FIELD` at it. Then:\n\n- items resolve by that slug in every read and write tool,\n- `search_items`/`get_item` summaries surface it, and\n- `field_index()` defaults to it, giving you a one-call dedupe map before a bulk\n  import.\n\nThis is entirely optional — leave `HOMEBOX_ALIAS_FIELD` unset and items resolve\nby assetId or name only. Nothing about the field name is special; any single\ncustom field works.\n\n## Companion scripts\n\nTwo standalone helper scripts ship in the repo (not MCP tools):\n\n- **`annotate_location_photos.py`** turns one wide photo of a shelf bank into one\n  boxed/labeled image per shelf (`--rows N`), and can upload each straight to its\n  Homebox location with `--upload`. See its `--help`.\n- **`heic2jpg.py`** converts HEIC/HEIF (iPhone photos) to JPEG for ad-hoc use\n  (`./heic2jpg.py FILE...`). Note the attach tools **already auto-convert HEIC on\n  upload**, so this is only needed to feed HEIC frames to a vision step *before*\n  upload.\n\nBoth rely on `pillow-heif`, which bundles libheif — there's no system package to\ninstall.\n\n## Security\n\n- **API key handling.** In the clone workflow the key lives in `.env`, which is\n  gitignored and should be mode `600` (`chmod 600 .env`). Never commit it. In the\n  `uvx` workflow it's passed through your MCP client's config — treat that file\n  as a secret too.\n- **Key rotation.** Rotating the Homebox `HBOX_AUTH_API_KEY_PEPPER` invalidates\n  every issued key — mint a new one and update your config.\n- **Confirm-gated deletes.** All four delete tools require `confirm` to equal the\n  target's exact name and carry the MCP `destructiveHint` annotation, so a\n  well-behaved client prompts before any destructive call.\n- **Use HTTPS** for any remote Homebox instance — the API key is sent on every\n  request in the `Authorization` header.\n\n## Development\n\n```bash\ngit clone https://github.com/dgahagan/homebox-mcp\ncd homebox-mcp\n\n# Run the test suite (httpx mocked with respx — no live instance needed)\nuv run --with pytest --with respx --with . pytest\n\n# Lint\nuv run --with ruff ruff check .\n```\n\nThe server is a single module (`homebox_mcp.py`) with PEP 723 inline\ndependencies. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and PR\nexpectations, and [CHANGELOG.md](CHANGELOG.md) for release notes.\n\n## License\n\n[MIT](LICENSE) © Dan Gahagan\n",
  "bytes": 17688,
  "sha": "e7e7091c3cef094a2608e39e1d1e5c2b8c801bc985fa32135bfb2c541d4278c6",
  "repo_slug": "dgahagan/homebox-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_dgahagan_homebox_mcp_a4443e67/readme"
}