Homebox MCP Server
by Tafeen
README.md
# Homebox MCP Server
An [MCP](https://modelcontextprotocol.io/) server that exposes
[Homebox](https://github.com/sysadminsmedia/homebox) inventory management as
tools for AI agents.
## What it does
This server lets an AI agent (Claude, Hermes, OpenClaw, or any MCP-compatible
client) read and write your Homebox inventory: search, add, update, and delete
products and locations, manage tags and maintenance entries — all through the
unified Homebox v0.26+ entities API.
It runs in two transports:
- **stdio** — for local agents that launch the server as a subprocess.
- **Streamable HTTP** (`/mcp`) — for remote agents that talk HTTP. Per-request
credentials are passed via request headers.
Key behaviors:
- Targets Homebox **v0.26.0+** (the *entity-merge* release) — items and
locations share `/api/v1/entities`, with `parentId` denoting the parent
entity. Older Homebox releases using `/api/v1/items` and `/api/v1/locations`
are **not** supported.
- `search_products` with only `name` splits the phrase into terms and queries
each in parallel for broader recall.
- `add_product` and `update_product_by_id` use grouped parameter objects
(`identity`, `location`, `identifiers`, `purchase`, `metadata`, `status`)
so large calls stay readable.
- Maintenance tools (`create_maintenance_entry_for_product`,
`list_maintenance_entries`, `update_maintenance_entry`,
`delete_maintenance_entry`) operate against `/api/v1/maintenance`.
- Multi-collection (tenant) support is exposed via the Homebox-native
`X-Tenant` request header on the HTTP transport.
## Installation
### Requirements
| Component | Version |
| ------------------------ | ---------------------------------------- |
| Homebox (server) | **v0.26.0 or newer** (tested v0.26.2) |
| Python | 3.10.13+ |
| Dependencies | Listed in `pyproject.toml` |
| `uv` (recommended) | https://docs.astral.sh/uv/ |
Older Homebox releases that still expose `/api/v1/items` and
`/api/v1/locations` are **not** supported — the server talks to
`/api/v1/entities` exclusively.
### Install the server
```bash
git clone <repo-url> homebox-mcp
cd homebox-mcp
uv sync # installs runtime + dev dependencies
cp .env.example .env # then edit .env with your Homebox credentials
```
`.env.example`:
```
HOMEBOX_BASE_URL=https://your-homebox-instance.com
HOMEBOX_USERNAME=your_email@example.com
HOMEBOX_PASSWORD=your_password
# Optional HTTP server settings (server_http.py)
# FASTMCP_HOST=0.0.0.0
# FASTMCP_PORT=8000
```
### Docker
```bash
docker build -t homebox-mcp .
docker run -p 8000:8000 \
-e HOMEBOX_BASE_URL="https://your-homebox-instance.com" \
-e HOMEBOX_USERNAME="your_email@example.com" \
-e HOMEBOX_PASSWORD="your_password" \
homebox-mcp
```
### Run the server
```bash
just start # stdio mode (default for local agents)
just start-http # Streamable HTTP mode at http://0.0.0.0:8000/mcp
```
Or directly:
```bash
uv run main.py # stdio
uv run server_http.py # streamable HTTP
```
## Install inside an agent
Pick the snippet that matches your agent and copy-paste it into the agent's
MCP configuration. Replace the values in **`<…>`** with your own.
The examples assume stdio mode (local agent) with credentials read from
environment / `.env`. Streamable HTTP mode exposes the same tools but takes
credentials through request headers instead.
### Claude Desktop / Claude Code (`claude_desktop_config.json`)
```json
{
"mcpServers": {
"homebox": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/homebox-mcp", "run", "main.py"],
"env": {
"HOMEBOX_BASE_URL": "https://your-homebox-instance.com",
"HOMEBOX_USERNAME": "your_email@example.com",
"HOMEBOX_PASSWORD": "your_password"
}
}
}
}
```
### Hermes (stdio)
Add to your Hermes MCP servers configuration:
```json
{
"homebox": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/homebox-mcp", "run", "main.py"],
"env": {
"HOMEBOX_BASE_URL": "https://your-homebox-instance.com",
"HOMEBOX_USERNAME": "your_email@example.com",
"HOMEBOX_PASSWORD": "your_password"
}
}
}
```
### OpenClaw (stdio)
```json
{
"servers": {
"homebox": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/homebox-mcp", "run", "main.py"],
"env": {
"HOMEBOX_BASE_URL": "https://your-homebox-instance.com",
"HOMEBOX_USERNAME": "your_email@example.com",
"HOMEBOX_PASSWORD": "your_password"
}
}
}
}
```
### Remote / Streamable HTTP
If you run the server with `uv run server_http.py` (or via the Docker image),
any MCP-compatible HTTP client can reach it at `http://host:8000/mcp` and pass
credentials via request headers:
```
HOMEBOX_USERNAME: your_email@example.com
HOMEBOX_PASSWORD: your_password
X-Tenant: 00000000-0000-0000-0000-000000000000 # optional, multi-collection
```
`HOMEBOX_BASE_URL` is read only from the runtime environment of the server
process; it cannot be overridden per request.
### Finding the `X-Tenant` value
Open Homebox, log in, then in DevTools → Network find the request
`GET /api/v1/users/self` and copy the `X-Tenant` request header. Pass it on
every MCP request via the `X-Tenant` header.
## Available tools
17 tools are exposed. Grouped parameter objects accept both snake_case and
camelCase keys.
### Products (entities)
#### `search_products`
Search products via Homebox query plus local filters. Provide at least one
filter; `name` is split into per-term queries when no other strict filter is
provided.
- `name` (Optional[str]) — product name (or phrase) query
- `item_id` (Optional[str]) — exact product UUID
- `asset_id` (Optional[str]) — asset ID filter
- `serial_number` (Optional[str])
- `model_number` (Optional[str])
- `manufacturer` (Optional[str])
- `query` (Optional[str]) — generic full-text query
**Returns:** list of matching product dicts, or an error string.
#### `add_product`
Create a new product or subitem with grouped parameters. If both `location`
and `parent_item` are provided, `parent_item` wins (subitem creation).
- `identity` (dict, required): `{"name", "description", "asset_id"}`
- `location` (Optional[dict]): `{"id"}` or `{"name"}` — for standalone products
- `parent_item` (Optional[dict]): `{"id"}` or `{"name"}` — for subitems
- `identifiers` (Optional[dict]): `{"serial_number", "model_number", "manufacturer"}`
- `purchase` (Optional[dict]): `{"price", "seller", "date", "warranty_expires"}`
- `metadata` (Optional[dict]): `{"notes", "quantity", "tag_ids", "tag_names", "custom_fields"}`
- `status` (Optional[dict]): `{"archived", "insured", "lifetime_warranty"}`
**Returns:** created product dict (after optional update pass), or error string.
#### `update_product_by_id`
Update an existing product. Pass IDs (`item_id`, `location.id`) — names are
only resolved through `list_locations` outside this tool.
- `item_id` (str, required) — UUID of the item to update
- `identity` (Optional[dict]): `{"new_name", "description", "asset_id"}`
- `location` (Optional[dict]): `{"id"}` or `{"name"}`
- `identifiers` (Optional[dict]): `{"serial_number", "model_number", "manufacturer"}`
- `purchase` (Optional[dict]): `{"price", "seller", "date", "warranty_expires"}`
- `metadata` (Optional[dict]): `{"notes", "quantity", "tag_ids", "tag_names", "custom_fields"}`
- `status` (Optional[dict]): `{"archived", "insured", "lifetime_warranty"}`
**Returns:** updated product dict, or error string.
#### `add_bulk_products`
Add multiple products in one call. Each entry uses the grouped shape from
`add_product`.
- `items` (list[dict], required) — each item must contain an `identity.name`
- **Returns:** list (one result per input item).
#### `delete_product_by_id`
Delete a single product by ID.
- `item_id` (str, required) — UUID of the item
- `product_name` (str, required) — used for the success/error message only
**Returns:** success message, or error string.
#### `delete_bulk_products`
Delete multiple products. Delegates to `delete_product_by_id` for each entry.
- `items` (list[dict], required) — each entry `{item_id, product_name}`
- **Returns:** list (one result message per input item).
### Locations
#### `list_locations`
Return Homebox's location **tree** (`GET /api/v1/entities/tree`).
- No parameters.
- **Returns:** list of top-level tree nodes (each with nested `children`).
#### `create_location`
Create a new location (entity with `isLocation` implied by being parent-less).
- `name` (str, required)
- `description` (Optional[str])
- `parent_id` (Optional[str]) — UUID of a parent location
**Returns:** created location dict, or error string.
#### `update_location_by_id`
Update a location's editable fields. Fetches the current state first to
preserve omitted fields.
- `location_id` (str, required) — UUID of the location
- `new_name` (Optional[str])
- `new_description` (Optional[str])
- `new_parent_id` (Optional[str]) — pass empty string `""` to detach from parent
**Returns:** updated location dict, or error string.
#### `delete_location_by_id`
Delete a single location.
- `location_id` (str, required) — UUID of the location
- **Returns:** success message, or error string.
#### `list_items_by_location`
List direct child items of a location (uses `?parentIds=…`).
- `location_id` (str, required) — UUID of the location
- **Returns:** list of `{"id", "name"}` items, or error string.
### Tags
#### `list_tags`
Return all tags from `/api/v1/tags`.
- No parameters.
- **Returns:** list of `{"id", "name"}` tags, or error string.
#### `create_tag`
Create a tag if it does not already exist (case-folded de-dupe).
- `name` (str, required)
- **Returns:** created or pre-existing tag dict, or error string.
### Maintenance
#### `create_maintenance_entry_for_product`
Create a maintenance entry for an existing product. Provide
`scheduled_date`, `completed_date`, or both; if neither, both default to
today.
- `product_id` (Optional[str]) — preferred if known
- `product_name` (Optional[str]) — resolved to a product ID if `product_id`
is missing
- `name` (str, required) — title/summary of the task
- `description` (Optional[str])
- `cost` (Optional[float])
- `scheduled_date` (Optional[str]) — `YYYY-MM-DD`
- `completed_date` (Optional[str]) — `YYYY-MM-DD`
**Returns:** created maintenance entry dict, or error string.
#### `list_maintenance_entries`
List maintenance entries from Homebox. The global `/api/v1/maintenance`
endpoint is broken on Homebox v0.26+; this tool falls back to enumerating
per-entity maintenance lists and aggregating them.
- `product_id` (Optional[str]) — restrict to entries for one product
- `product_name` (Optional[str]) — used to resolve the entity when
`product_id` is missing
- **Returns:** list of maintenance entry dicts, or error string.
#### `update_maintenance_entry`
Update an existing maintenance entry. Homebox v0.26+ exposes maintenance
only per-entity, so the owning product must be supplied.
- `entry_id` (str, required) — UUID of the entry
- `product_id` (Optional[str]) — UUID of the owning entity (preferred)
- `product_name` (Optional[str]) — resolved to an entity when `product_id`
is missing
- `name` (Optional[str])
- `description` (Optional[str])
- `cost` (Optional[float])
- `scheduled_date` (Optional[str]) — `YYYY-MM-DD`
- `completed_date` (Optional[str]) — `YYYY-MM-DD`
**Returns:** updated maintenance entry dict, or error string.
#### `delete_maintenance_entry`
Delete a maintenance entry.
- `entry_id` (str, required) — UUID of the entry
- **Returns:** success message, or error string.
## Testing
The project ships unit tests and end-to-end tests.
### Unit tests (no Docker required)
```bash
just test
# or: uv run pytest tests/ -v -k "not e2e"
```
### End-to-end tests (requires Docker)
The E2E suite spins up a **pinned Homebox v0.26.2** container, builds the MCP
server image, registers a test user, and exercises **all 17 tools** through the
Streamable HTTP transport. Each tool's effect is then **verified against the
real Homebox API** (not just the MCP response envelope).
```bash
just test-e2e
```
What it does:
1. `docker compose -f docker-compose.e2e.yml up -d` — start Homebox v0.26.2
on port `31745` and the MCP server on port `31746`.
2. Wait for the Homebox healthcheck.
3. Register a test user (idempotent).
4. Run `pytest tests/e2e/ -m e2e`.
5. Tear down all containers.
See `docker-compose.e2e.yml` for the pinned image and `tests/e2e/` for the
suite. The suite covers every tool:
| Category | Tools tested |
| --- | --- |
| Locations | `list_locations`, `create_location`, `update_location_by_id`, `list_items_by_location`, `delete_location_by_id` |
| Products | `search_products`, `add_product`, `update_product_by_id`, `add_bulk_products`, `delete_product_by_id`, `delete_bulk_products` |
| Tags | `list_tags`, `create_tag` |
| Maintenance | `create_maintenance_entry_for_product`, `list_maintenance_entries`, `update_maintenance_entry`, `delete_maintenance_entry` |
## Recommended workflow for adding items
1. **Find a location for a standalone product** — call `list_locations` to get
a location UUID.
2. **Find a parent for a subitem** — call `search_products(name=...)` to
resolve the parent item UUID.
3. **Create or update** — use `add_product` (new) or `update_product_by_id`
(existing). If both `location` and `parent_item` are passed to
`add_product`, `parent_item` is used and `location` is ignored for the
creation target.
## Justfile commands
```bash
just --list # Show all commands
just start # Start MCP server in stdio mode
just start-http # Start MCP server in Streamable HTTP mode
just stop # Stop running HTTP servers
just lint-format # Run linting checks
just lint-fix # Run linting and auto-fix issues
just test # Run unit tests
just test-e2e # Run E2E tests (requires Docker)
just wipe # Clean cache/build artifacts
just docker-build # Build Docker image
just docker-run # Run Docker container
```
## Repository layout
```
homebox-mcp/
├── main.py # FastMCP server, all 17 tools, helpers
├── models.py # Grouped dataclasses for tool inputs
├── server_http.py # Streamable HTTP transport entrypoint
├── request_config_middleware.py # Per-request credential/tenant scoping
├── Dockerfile # HTTP server image
├── docker-compose.e2e.yml # Pinned Homebox + MCP server stack
├── justfile # Dev/test commands
├── scripts/e2e-setup.sh # Test user registration helper
├── tests/
│ ├── test_main.py # Unit tests (mocked HTTP)
│ ├── test_request_config_middleware.py
│ ├── test_transport_servers.py
│ └── e2e/ # E2E tests against real Homebox v0.26.2
└── pyproject.toml
```
TDQS
C2.3/5.0
Scored across 17 tools
Disambiguation3/5
Most tools target distinct entities (products, locations, maintenance, tags), but 'add_bulk_products' and 'add_product' could be confused without description. Several tools lack descriptions, reducing clarity.
Naming Consistency2/5
Names mix patterns: 'add_product', 'create_location', 'create_maintenance_entry_for_product', 'delete_bulk_products', 'list_items_by_location'. Inconsistent verb styles and lengths.
Tool Count4/5
17 tools is reasonable for an inventory server covering products, locations, maintenance, and tags. Slightly on the higher end but still well-scoped.
Completeness3/5
CRUD for main entities is present, but missing explicit get-by-ID tools for products and locations. Many tools have no descriptions, creating dead ends for agents.
Maintenance
ActivityInactive
ResponsivenessUnresponsive