inventory-mcp
by golem15com
README.md
# inventory-mcp
MCP server for **Inventory** ([whereiput.it](https://whereiput.it)) — search your stuff, do full CRUD, and run the AI photo-assist flow from Claude Desktop, Claude Code, Codex, or any MCP-compatible client.
**This is software you run yourself, against your own Inventory server.** There is no hosted MCP
endpoint and no default API URL — you point it at the `/api/v1/inventory/*` token API of *your own*
whereiput.it instance (running on your machine, your LAN, or your droplet) and run it next to your
MCP client. Nothing here phones home.
It is a **thin scope-enforced proxy** over the Inventory token API (`/api/v1/inventory/*`). It makes
**no auth decision**: it forwards your `inv_` bearer unchanged as an `Authorization: Bearer` header,
and the API's 403/404 is the real boundary. The token is never placed in a URL and never logged.
> **Two distinct URLs — don't conflate them.**
> - **The MCP endpoint** is what you configure into your *client*: either a `node dist/stdio.js`
> command (stdio) or an `http://YOUR_HOST:3100/mcp` URL (HTTP transport).
> - **`INVENTORY_API_URL`** is what this *server* proxies to — *your* Inventory API host. It is
> **required, has no default**, and the server exits immediately if it is unset.
## Get a token
Mint a personal API token in the SPA of **your own** instance at
**`{YOUR_WHEREIPUT_APEX}/settings?tab=integrations`** (name + scopes `read`/`write`/`ai`) — replace
`{YOUR_WHEREIPUT_APEX}` with your instance's address, e.g. `http://localhost:8088` or
`https://inventory.yourdomain`. The raw `inv_…` secret is shown once — copy it into the installer.
Tool visibility is gated by the token's scopes: a `read`-only token never sees write/ai tools.
## Setup
```bash
npm install
npm run build # compiles dist/ AND regenerates the host-agnostic install.sh
```
## stdio (zero infra — the default path)
stdio runs on the **same machine as your MCP client** with **no port, no TLS, and no process
manager** — the client launches `node dist/stdio.js` on demand. This is the recommended path.
Claude Desktop config (`claude_desktop_config.json`). Pick the `INVENTORY_API_URL` that matches
*your* server topology (Phase 17): **single-origin** (API + SPA on one host) or **split api-host**
(the `/api/v1/inventory/*` routes served by the Laravel backend on a separate `api.` host):
```jsonc
{
"mcpServers": {
"inventory": {
"command": "node",
"args": ["/path/to/inventory-mcp/dist/stdio.js"],
"env": {
// Single-origin (API + SPA together):
"INVENTORY_API_URL": "http://localhost:8088",
// Split api-host (uncomment instead — the backend serves /api/v1/inventory on api.):
// "INVENTORY_API_URL": "https://api.yourdomain",
"INVENTORY_API_TOKEN": "inv_your_token_here"
}
}
}
}
```
Here the `command` + `args` (`node dist/stdio.js`) *are* the MCP endpoint; `INVENTORY_API_URL` is the
separate server-side proxy target. http is fine for a local/private API; use https if your API is
publicly reachable.
## HTTP transport (LAN / droplet)
Run the HTTP transport when you want to share one MCP server across devices — e.g. on your local
network next to Home Assistant, or on the same droplet that runs your whereiput.it instance:
```bash
INVENTORY_API_URL=http://localhost:8088 node dist/http.js # or https://api.yourdomain
```
It listens on `INVENTORY_MCP_PORT` (default `3100`) and accepts the `inv_` bearer **per request**
(no `INVENTORY_API_TOKEN` on the server). **http is allowed only for local/private networks; if you
expose the transport publicly, https is REQUIRED** — the Node process speaks plain HTTP, so
**TLS-terminate it yourself** behind a reverse proxy you control.
Wire it into a client:
```bash
# Claude Code
claude mcp add inventory --transport http http://YOUR_HOST:3100/mcp -H "Authorization: Bearer inv_…"
```
```toml
# Codex (~/.codex/config.toml)
[mcp_servers.inventory]
url = "http://YOUR_HOST:3100/mcp"
bearer_token_env_var = "INVENTORY_TOKEN"
```
```bash
export INVENTORY_TOKEN="inv_your_token_here" # fish: set -gx INVENTORY_TOKEN inv_your_token_here
```
```jsonc
// Generic HTTP client
{ "type": "http", "url": "http://YOUR_HOST:3100/mcp", "headers": { "Authorization": "Bearer inv_…" } }
```
### Self-host install helper
When the HTTP transport is running, its `/` route serves a host-agnostic `install.sh` that advertises
**its own address** (derived from the request) and prompts for token + client + `INVENTORY_API_URL`:
```bash
curl -fsSL http://YOUR_HOST:3100 | bash
# non-interactive (token, client, api-url positional):
curl -fsSL http://YOUR_HOST:3100 | bash -s -- inv_your_token_here codex http://localhost:8088
```
The same `install.sh` is generated on disk by `npm run build`, so you can also run `./install.sh`
directly from the clone. It gates on the `inv_` prefix and configures Claude Code (`claude mcp add`)
and/or Codex (`codex mcp add` + an idempotent managed-block shell export) — never embedding the token
in a URL.
## Optional: Docker
A multi-stage `node:22-alpine` `Dockerfile` (running the **HTTP transport**) ships in this repo. It
bakes **no** `INVENTORY_API_URL` — you supply it at run time:
```bash
docker build -t inventory-mcp .
docker run -e INVENTORY_API_URL=http://host.docker.internal:8088 -p 3100:3100 inventory-mcp
```
An optional, opt-in compose snippet (explicitly **not** part of the main self-host
`docker-compose.yml` and **not** wired behind the edge proxy) is documented in
[`docs/SELF-HOSTING.md`](../docs/SELF-HOSTING.md) under *Optional: Inventory MCP server*.
## Environment variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `INVENTORY_API_URL` | **Required — no default** | *Your* Inventory token API base URL. Single-origin `http://localhost:8088` **or** split api-host `https://api.yourdomain`. The server exits with a fail-fast message if unset. **Never** a hosted default. |
| `INVENTORY_API_TOKEN` | — | The `inv_` bearer (**stdio only**; the HTTP transport takes it per-request). |
| `INVENTORY_MCP_PORT` | `3100` | HTTP transport listen port. |
## Tools
| Tool | Scope | API call |
|------|-------|----------|
| `list_areas`, `get_area` | read | GET areas[/{id}] |
| `list_locations`, `get_location` | read | GET areas/{id}/locations, locations/{id} |
| `search_items`, `get_item` | read | GET items/search?q=, items/{id} |
| `create_area` / `update_area` / `delete_area` | write | POST/PUT/DELETE areas |
| `create_location` / `update_location` / `delete_location` | write | POST areas/{id}/locations, PUT/DELETE locations/{id} |
| `create_item` / `update_item` / `delete_item` | write | POST locations/{id}/items, PUT/DELETE items/{id} |
| `create_category`, `create_tag` | write | POST categories / tags |
| `recognize_photo` | ai | POST items/recognize |
| `save_recognized` | ai | POST locations/{id}/items/bulk |
A user-invoked `catalog_from_photo` prompt walks the recognize → review → save flow.
## AI photo-assist caveat (Phase 7 follow-up)
`recognize_photo` passes text-friendly args (`image_base64` + `mime` + `area_id`). The Phase 7
recognize endpoint is **multipart-only** today. For the remote HTTP transport, the recognize endpoint
likely needs a **base64/JSON input-adapter branch** (an input adapter only — no logic change). The
`ai` tools + client methods ship now; the live end-to-end recognize call lights up once that input
adapter lands. This is a documented sequencing acceptance, not a blocker.
## Development
```bash
npm run dev # HTTP transport with tsx (no build step)
npm run dev:stdio # stdio transport with tsx
npm test # vitest
```
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues