Skip to main content
Glama
Tafeen

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