Content
# 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
```
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available