Snipe-IT MCP Server
# Snipe-IT MCP Server
A comprehensive [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for managing [Snipe-IT](https://snipeitapp.com/) inventory systems. This server enables AI assistants to perform full CRUD operations across your entire Snipe-IT instance with **40 tools** covering all major API endpoints, plus **2 optional data-quality tools** that use a judgment model — [TypeSafe Jev](https://typesafe.ai/), a self-hosted open-weight System One model such as Laya, or an OpenAI-compatible chat model via Ollama — for duplicate detection and record matching.
## Features
### Asset Management
- **Full CRUD Operations**: Create, read, update, delete, and search assets with enhanced filtering
- **Barcode/Serial Lookup**: Direct bytag/byserial API endpoints for reliable barcode scanning
- **Asset Operations**: Checkout, checkin, audit, and restore assets
- **Checkout Requests**: Submit and cancel checkout requests for requestable assets
- **File Attachments**: Upload, download, list, and delete asset files
- **Label Generation**: Generate printable PDF labels
- **Maintenance Tracking**: Create and manage maintenance records
- **License Associations**: View licenses assigned to assets
### Inventory Tracking
- **Consumables**: Complete management of consumable items
- **Components**: Manage components with checkout/checkin to assets
- **Accessories**: Track accessories with checkout/checkin to users, assets, or locations
### Users & Organization
- **Users**: Full user management including restore and current user endpoint
- **User Assets**: View all items checked out to a user (assets, accessories, licenses, consumables, EULAs)
- **Two-Factor Auth**: Reset user 2FA (admin function)
- **Companies**: Multi-tenant company management
- **Departments**: Organizational department management
- **Groups**: Permission group management
### System Configuration
- **Categories**: Manage categories for all item types
- **Manufacturers**: Track manufacturer information
- **Models**: Define asset models with depreciation, custom fields, and file attachments
- **Status Labels**: Configure asset statuses with asset listing
- **Locations**: Manage physical locations with hierarchy, asset/user queries
- **Suppliers**: Track supplier information
- **Depreciations**: Define depreciation schedules
### Custom Fields
- **Fields**: Create and manage custom field definitions
- **Fieldsets**: Group custom fields for assignment to models with field reordering
- **Field Association**: Associate/disassociate fields with fieldsets
### Licensing
- **License Management**: Full CRUD for software licenses
- **Seat Assignments**: Checkout/checkin license seats
- **License Files**: Manage license documentation
### Reporting & Auditing
- **Activity Logs**: Query all activity history
- **Item Activity**: Get activity for specific items
- **Status Summary**: Asset counts grouped by status label
- **Audit Tracking**: Track assets due/overdue for audit
### Data Quality (optional, judgment-model backed)
- **Duplicate Detection**: Find likely duplicate manufacturers, models, suppliers, locations, categories, companies, or departments — judged pairwise with a confidence per pair
- **Record Matching**: Resolve free-text names (CSV cells, spoken descriptions) to existing record IDs, with an explicit "none" abstention
- **Bring your own model**: TypeSafe Jev (hosted), any self-hosted server speaking the System One protocol (Laya, CLM, …), or any OpenAI-compatible chat server (Ollama, vLLM, LM Studio, …)
- Enabled only when a backend is configured; see [Optional — judgment backend for the data-quality tools](#optional--judgment-backend-for-the-data-quality-tools)
### Import & System Administration
- **CSV Imports**: Full import workflow (upload, map columns, process)
- **System Info**: Get Snipe-IT version information
- **Backups**: List and download database backups
- **LDAP Operations**: LDAP sync and connection testing
## Requirements
- Python 3.11+
- [UV](https://github.com/astral-sh/uv) package manager
- Snipe-IT instance with API access
- API token with appropriate permissions
## Installation
### 1. Clone the repository
```bash
git clone https://github.com/jameshgordy/snipeit-mcp.git
cd snipeit-mcp
```
### 2. Install dependencies
```bash
uv sync
```
### 3. Configure environment variables
The server supports two authentication modes; pick one.
#### Mode A — API key (stdio or HTTP, single shared identity)
Create a `.env` file:
```env
SNIPEIT_URL=https://your-snipeit-instance.com
SNIPEIT_TOKEN=your-api-token-here
SNIPEIT_ALLOWED_TOOLS=manage_assets,system_info # Optional: restrict exposed tools
```
| Variable | Required | Description |
|----------|----------|-------------|
| `SNIPEIT_URL` | Yes | Your Snipe-IT instance URL |
| `SNIPEIT_TOKEN` | Yes | API token for authentication |
| `SNIPEIT_ALLOWED_TOOLS` | No | Comma-separated list of tool names to expose. If unset, all tools are available. |
**Getting an API Token:**
1. Log in to your Snipe-IT instance
2. Navigate to your user profile (top right menu)
3. Go to "Manage API Keys" or "Personal Access Tokens"
4. Generate a new token with required permissions
#### Mode B — Interactive OAuth login (HTTP only, per-user identity)
In this mode the MCP server runs as a web service and acts as an OAuth proxy in
front of Snipe-IT's built-in [Laravel Passport](https://snipe-it.readme.io/) provider.
Each user logs in to Snipe-IT (going through your normal SAML / SSO if configured)
and the MCP server uses that user's own access token for every tool call.
**One-time Snipe-IT setup** (admin):
1. Visit `https://your-snipeit-instance.com/admin/oauth`
2. Create a new OAuth client
3. Set the redirect URI to `https://your-mcp-public-url/auth/callback`
4. Note the generated client ID and secret
**Environment variables:**
```env
SNIPEIT_URL=https://your-snipeit-instance.com
SNIPEIT_OAUTH_CLIENT_ID=... # from /admin/oauth
SNIPEIT_OAUTH_CLIENT_SECRET=... # from /admin/oauth
SNIPEIT_MCP_BASE_URL=https://your-mcp-public-url
MCP_TRANSPORT=http
MCP_PORT=8000
# MCP_HOST=0.0.0.0 # defaults to 127.0.0.1
```
| Variable | Required | Description |
|----------|----------|-------------|
| `SNIPEIT_URL` | Yes | Your Snipe-IT instance URL |
| `SNIPEIT_OAUTH_CLIENT_ID` | Yes | OAuth client ID from `/admin/oauth` |
| `SNIPEIT_OAUTH_CLIENT_SECRET` | Yes | OAuth client secret from `/admin/oauth` |
| `SNIPEIT_MCP_BASE_URL` | Yes | Public URL where this MCP server is reachable (used in the OAuth callback) |
| `SNIPEIT_MCP_REDIRECT_PATH` | No | Override OAuth callback path (default `/auth/callback`) |
| `MCP_TRANSPORT` | Yes | Must be `http` for OAuth mode |
| `MCP_HOST` | No | Bind address (default `127.0.0.1`; use `0.0.0.0` behind a reverse proxy) |
| `MCP_PORT` | Yes | TCP port for the HTTP server |
| `LOG_LEVEL` | No | `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL` (default `INFO`) |
> [!NOTE]
> OAuth mode requires HTTP transport — starting with `MCP_TRANSPORT=stdio`
> while OAuth env vars are set fails at startup with a clear error.
#### Mode C — multi-identity (HTTP, one container, per-user tokens)
In this mode **one container serves all people**, each still acting under
their own Snipe-IT personal access token. Instead of one container per
person (each with a private `SNIPEIT_TOKEN`), every person gets a random
**MCP token** that they put into their own MetaMCP server entry. The server
maps `MCP token → identity → Snipe-IT PAT` per request, so the Snipe-IT
activity log shows the correct person for every action, and the container
log carries a second, Snipe-IT-independent audit trail.
Mode C requires HTTP transport and is **mutually exclusive with OAuth mode**
(startup fails if both are configured).
**Per-person tokens:** generate one random token per person, e.g.
`openssl rand -hex 32`. Tokens must be at least 32 characters, unique per
person, and different from that person's Snipe-IT PAT (startup validates
all of this and exits with a clear error otherwise).
**Environment variables:**
```env
SNIPEIT_URL=https://your-snipeit-instance.com # one instance for all
# One block per person (<KEY> matches [A-Z0-9_]+, e.g. STAAT):
SNIPEIT_IDENTITY_STAAT_MCP_TOKEN=... # required, >= 32 chars, unique
SNIPEIT_IDENTITY_STAAT_SNIPEIT_TOKEN=... # required, the person's Snipe-IT PAT
SNIPEIT_IDENTITY_STAAT_DISPLAY_NAME=... # optional, shown in logs
SNIPEIT_IDENTITY_STAAT_ALLOWED_TOOLS=... # optional CSV, e.g. manage_assets,system_info
SNIPEIT_IDENTITY_STAAT_READ_ONLY=true # optional, blocks all write actions
# Transport (required for this mode):
MCP_TRANSPORT=http
MCP_PORT=8000
MCP_HOST=127.0.0.1
```
| Variable | Required | Description |
|----------|----------|-------------|
| `SNIPEIT_URL` | Yes | Snipe-IT instance URL (shared by all identities) |
| `SNIPEIT_IDENTITY_<KEY>_MCP_TOKEN` | Yes | Random bearer token the person uses to authenticate to this MCP server (min 32 chars, unique per person) |
| `SNIPEIT_IDENTITY_<KEY>_SNIPEIT_TOKEN` | Yes | That person's Snipe-IT personal access token |
| `SNIPEIT_IDENTITY_<KEY>_DISPLAY_NAME` | No | Human-readable name used in log lines (identity keys are logged at startup, never token values) |
| `SNIPEIT_IDENTITY_<KEY>_ALLOWED_TOOLS` | No | Comma-separated tool allowlist for that person; enforced at call time **and** in `tools/list` |
| `SNIPEIT_IDENTITY_<KEY>_READ_ONLY` | No | When true, blocks all write actions (`create`, `update`, `delete`, `checkout`, `checkin`, `audit`, `restore`, file uploads, imports, LDAP sync, …); read actions stay allowed |
| `SNIPEIT_IDENTITIES_FILE` | No | Path to a JSON file with the same fields (list of objects with an explicit `key`). **If set, the file wins and all `SNIPEIT_IDENTITY_*` env vars are ignored** (logged at startup). For secret-mount based environments. |
| `MCP_TRANSPORT` | Yes | Must be `http` |
| `MCP_PORT` | Yes | TCP port for the HTTP server |
| `MCP_HOST` | No | Bind address (default `127.0.0.1`) |
| `LOG_LEVEL` | No | `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL` (default `INFO`) |
**MetaMCP configuration — one entry per person:** the only difference
between the entries is the bearer token; the URL is identical for everyone.
```text
MetaMCP server entry (per person, Ownership: Private)
Name: snipeit
Type: Streamable HTTP
URL: http://snipeit-mcp:8000/mcp ← identical for all
Bearer Token: <that person's MCP token> ← the only difference
```
**How a request is handled:**
1. The HTTP layer checks `Authorization: Bearer <mcp_token>`. Missing or
unknown token → `401` with `WWW-Authenticate: Bearer`, before any tool
runs. Token comparison is constant-time (`hmac.compare_digest`).
2. The identity is stored in a request-scoped context variable; the
tool call then authenticates to Snipe-IT with **that person's** PAT.
3. One JSON audit line per tool call goes to stderr (logger
`snipeit_mcp.audit`): identity, tool, action, outcome, duration, and a
SHA-256 digest of the arguments (arguments themselves are never logged,
and no token ever appears in logs).
```json
{"ts":"2026-08-26T09:41:02.113456+00:00","identity":"STAAT","tool":"manage_assets","action":"update","ok":true,"duration_ms":142,"args_digest":"9f2c1ab04e6d7731"}
```
**Health endpoint:** unauthenticated `GET /healthz` returns
`{"status":"ok","identities":<n>}` — use it for container healthchecks
(see `deploy/docker-compose.yml`).
> [!WARNING]
> **Known, accepted limitations** — read before deploying:
>
> - The container still holds **all** people's PATs in environment
> variables. Whoever can read them (Portainer admin, Docker host access)
> can impersonate any identity. Per-request attribution becomes
> robust, but it is **not tamper-proof**.
> - One container for everyone is a **single point of failure** for all of
> them — if it goes down, nobody's Snipe-IT access via MCP works.
> - Bearer-token authentication only protects traffic that stays on the
> internal Docker network. **Do not publish the port** to a wider
> network (the compose file deliberately has no `ports:` mapping).
> - Only OAuth mode (Mode B) provides cryptographically sound per-user
> attribution, and it does not fit centrally managed clients like
> MetaMCP.
#### Optional — judgment backend for the data-quality tools
The two data-quality tools, `find_duplicates` and `match_records`, ask *typed
questions* — "are these two records the same manufacturer?" (a 3-level score),
"which of these models does this text refer to?" (a choice with a `none`
option) — and need a model to answer them with a probability per option. They
are **hidden from `tools/list` until a backend is configured**, so an
unconfigured server presents exactly the 40 core tools. They work in every auth
mode and are read-only with respect to Snipe-IT.
| Backend (`JUDGMENT_BACKEND`) | Works with | Confidence | Speed / cost |
|------------------------------|-----------|------------|--------------|
| `systemone` (default) | [TypeSafe Jev](https://typesafe.ai/) (hosted, needs a key from [console.typesafe.ai](https://console.typesafe.ai/keys)); self-hosted open-weight *System One* models that serve the same `POST /v1/systemone` protocol — [Laya](https://systemonemodels.org/models/laya/) (Apache 2.0, ~400M params), CLM-8B, [Rapid-MLX](https://github.com/raullenchai/Rapid-MLX)'s system-one server, [openjev](https://github.com/razorback16/openjev) | **Calibrated** probabilities; the thresholds below are meaningful | ~0.1 s per question set; Jev is ~$0.04 per million input tokens, self-hosted is free |
| `openai` | Any OpenAI-compatible `/chat/completions` server with JSON-schema structured outputs: [Ollama](https://ollama.com) (≥ 0.5), vLLM, LM Studio, llama.cpp server, OpenRouter, OpenAI | **Not calibrated**: the model's self-reported confidence, or vote frequency across `JUDGMENT_SAMPLES` runs | Seconds per question set on a local 8B model; a 100-pair scan is minutes, not seconds |
> [!NOTE]
> Only the hosted TypeSafe API has been exercised for wire-format
> compatibility (against the official SDK's source). The self-hosted System One
> servers and the chat backend have been tested against mocked HTTP only —
> please report what works for you.
**TypeSafe Jev (hosted):**
```env
TYPESAFE_API_KEY=... # the typesafe-sdk variable name is accepted as-is
#TYPESAFE_DEFAULT_MODEL=jev-1.13.0 # pin a version once thresholds are tuned
```
**Self-hosted System One model (e.g. Laya):**
```env
JUDGMENT_BACKEND=systemone
JUDGMENT_BASE_URL=http://laya:8080 # must serve POST /v1/systemone
JUDGMENT_MODEL=laya
```
**Ollama (or any OpenAI-compatible server):**
```env
JUDGMENT_BACKEND=openai
JUDGMENT_BASE_URL=http://localhost:11434/v1 # Ollama's OpenAI-compatible base (default)
JUDGMENT_MODEL=qwen3:8b # any pulled model that follows JSON schemas well
#JUDGMENT_SAMPLES=3 # optional: vote across 3 samples instead of self-reported confidence
#JUDGMENT_API_KEY=sk-... # only for hosted services (OpenRouter, OpenAI, ...)
```
| Variable | Required | Description |
|----------|----------|-------------|
| `JUDGMENT_BACKEND` | No | `systemone` (default) or `openai`. Setting any `JUDGMENT_*` / `TYPESAFE_*` variable enables the tools. |
| `JUDGMENT_BASE_URL` | No | Default `https://api.typesafe.ai` for `systemone`, `http://localhost:11434/v1` for `openai`. Alias: `TYPESAFE_BASE_URL`. |
| `JUDGMENT_MODEL` | For `openai` | Default `jev-latest` for `systemone`; required for `openai` (e.g. `qwen3:8b`). Alias: `TYPESAFE_DEFAULT_MODEL`. Aliases like `jev-latest` move with releases; pin a version after tuning thresholds. |
| `JUDGMENT_API_KEY` | For hosted services | Sent as `Authorization: Bearer`. Omit for local servers. Alias: `TYPESAFE_API_KEY`. |
| `JUDGMENT_TIMEOUT` | No | Per-request timeout in seconds (default `10` for `systemone`, `120` for `openai`). Retries on 408/429/5xx and transport errors follow the official TypeSafe SDK policy (2 retries, exponential backoff, `Retry-After` honoured). Alias: `TYPESAFE_TIMEOUT`. |
| `JUDGMENT_SAMPLES` | No | `openai` only, 1–10 (default `1`). Above 1, each question set is asked that many times at a sampling temperature and vote frequency becomes the probability. Multiplies cost and latency. |
| `JUDGMENT_TIME_BUDGET` | No | Seconds one tool call may spend on backend calls in total (default `300`). Pairs or texts still pending at the deadline are skipped and listed in `errors`. |
> [!WARNING]
> **With a hosted backend, data leaves your network.** The projected fields of
> the scanned records — names, model numbers, addresses, and supplier/company
> contact URLs, emails and phone numbers — are sent to whatever
> `JUDGMENT_BASE_URL` points at. Self-host (Laya, Ollama, …) if that is not
> acceptable. Users are deliberately **not** supported by these tools because
> user records are personal data. For TypeSafe, review their
> [legal terms](https://docs.typesafe.ai/legal) (zero data retention is an
> enterprise option). In multi-identity mode, restrict who may call these tools
> with `SNIPEIT_IDENTITY_<KEY>_ALLOWED_TOOLS`.
**Cost and limits.** `find_duplicates` compares every pair when the table is
small (≤ `max_pairs` pairs) and otherwise pre-filters lexically similar pairs
in code (capped at 20 000 comparisons), so backend calls are bounded by
`max_pairs` (default 100, max 500). `match_records` sends at most
`candidates_per_text` records (default 50, max 254) plus `none` per text. Both
tools report `usage` token counts and `elapsed_seconds`, and stop submitting
work when `JUDGMENT_TIME_BUDGET` runs out.
**Reading the results.** Every judgment carries a `confidence` (0–1) and a
`confidence_band`: `high` (≥ 0.9) is safe to act on, `medium` (0.5–0.9) should
be confirmed by a person, `low` should not be acted on. Every result also says
who judged: `backend`, `model`, `calibrated` and `confidence_source` (`model`
for System One models, `self_reported` or `vote_frequency` for chat models).
The bands are only meaningful when `calibrated` is `true`; with a chat backend
treat them as a hint and keep a human in the loop. `find_duplicates` separates
`duplicates` (judged the same), `review` (related, possibly the same) and
`different_count`; treat `review` as a to-check list, never a to-merge list.
## Production Deployment
For running the server as a long-lived HTTPS service on a Linux VM (the typical
shape for OAuth mode), the repo ships two helper scripts under `scripts/`:
| Script | Purpose |
|--------|---------|
| [`scripts/setup-snipeit-mcp.sh`](scripts/setup-snipeit-mcp.sh) | One-shot installer. Creates a `snipeit-mcp` service user, installs [uv](https://github.com/astral-sh/uv) if missing, writes `/etc/snipeit-mcp.env` (seeding `SNIPEIT_*` values from a `.env` at the repo root if present), installs and starts a hardened systemd unit, and probes the local OAuth metadata endpoint. Idempotent. |
| [`scripts/update-snipeit-mcp.sh`](scripts/update-snipeit-mcp.sh) | Routine update — `git pull`, re-`uv sync`, restart the service, re-probe. |
**Quick-start on a fresh VM** (assumes Debian/Ubuntu with systemd; needs root):
```bash
# 1. Clone the source tree
sudo git clone https://github.com/jameshgordy/snipeit-mcp.git /opt/snipeit-mcp
# 2. (Optional) Drop a .env at the repo root so setup can seed
# SNIPEIT_URL / SNIPEIT_OAUTH_CLIENT_ID / _SECRET / SNIPEIT_MCP_BASE_URL.
# Missing values become __FILL_ME__ placeholders in /etc/snipeit-mcp.env.
scp .env you@vm:/tmp/snipeit-seed.env
sudo mv /tmp/snipeit-seed.env /opt/snipeit-mcp/.env
# 3. Install and start
sudo bash /opt/snipeit-mcp/scripts/setup-snipeit-mcp.sh
# 4. Future updates
sudo bash /opt/snipeit-mcp/scripts/update-snipeit-mcp.sh
```
> [!NOTE]
> The scripts are committed with the executable bit set, so
> `sudo /opt/snipeit-mcp/scripts/...` works once they're checked out via
> `git clone`. The `sudo bash ...` form above is the bullet-proof alternative
> — it doesn't care about file permissions, useful if you transferred the
> scripts via `scp`/drag-and-drop and the bit didn't come along.
**What the installer configures**:
| Path | Purpose |
|------|---------|
| `/opt/snipeit-mcp/` | Source tree (owned by service user) |
| `/var/lib/snipeit-mcp/` | `FASTMCP_HOME` — DCR'd MCP client registrations persist here |
| `/etc/snipeit-mcp.env` | Secrets and deployment-specific URLs (`SNIPEIT_*` only) |
| `/etc/systemd/system/snipeit-mcp.service` | systemd unit; infra settings (`MCP_TRANSPORT`, `MCP_HOST`, `MCP_PORT`, `LOG_LEVEL`, `FASTMCP_HOME`) are baked into its `Environment=` directives |
**Configurable at install time** via environment variables on the `setup-snipeit-mcp.sh` invocation:
| Variable | Default | Notes |
|----------|---------|-------|
| `SOURCE_DIR` | `/opt/snipeit-mcp` | Source tree path |
| `STATE_DIR` | `/var/lib/snipeit-mcp` | Service-user home / FASTMCP_HOME |
| `ENV_FILE` | `/etc/snipeit-mcp.env` | Generated env file |
| `SEED_ENV_FILE` | `$SOURCE_DIR/.env` | Optional seed for `SNIPEIT_*` values |
| `MCP_TRANSPORT` | `http` | Always `http` for OAuth mode |
| `MCP_HOST` | `127.0.0.1` | Bind address. Loopback by default; set `MCP_HOST=0.0.0.0` to expose on all interfaces (e.g. a reverse proxy on a different host) |
| `MCP_PORT` | `8000` | TCP port |
| `LOG_LEVEL` | `INFO` | |
> [!IMPORTANT]
> The scripts do **not** configure TLS — the server listens on plain HTTP on
> the chosen `MCP_PORT`. For public OAuth use, terminate TLS in front of it
> (corporate reverse proxy, Caddy, nginx, …) with a trusted certificate for
> the hostname in `SNIPEIT_MCP_BASE_URL`.
### Exposing a VPN-only Snipe-IT to web-based MCP clients (DMZ reverse proxy)
Web-based MCP clients (Claude.ai, Mistral's Le Chat, …) run their MCP transport
through the client vendor's own backend, which needs to reach
`SNIPEIT_MCP_BASE_URL` from the public internet — a VPN-only address won't
work. If your Snipe-IT instance itself is VPN-only, the typical shape is to
keep the MCP VM internal and put a **public-facing reverse proxy in a DMZ** in
front of it:
```text
Web client backend ──HTTPS──► public reverse proxy (DMZ) ──HTTP──► MCP VM (internal) ──HTTPS──► Snipe-IT (internal)
```
What the DMZ proxy needs:
- **Public hostname** matching `SNIPEIT_MCP_BASE_URL` (e.g. `snipeit.mcp.example.com`).
- **Trusted TLS certificate** for that hostname — client backends will not
accept internal CAs.
- **Upstream**: the internal VM on `http://<vm-ip>:<MCP_PORT>`.
- **Forwarded headers**: `Host`, `X-Forwarded-Proto: https`, `X-Forwarded-Host`,
`X-Forwarded-For`. FastMCP uses these to build correct OAuth metadata URLs.
- **Do not strip `WWW-Authenticate`** from upstream responses — MCP clients
(Inspector, `mcp-remote`, web clients) rely on it to discover the OAuth flow.
Header-allowlist proxies are a common culprit.
- **Do not add CORS headers** — FastMCP handles its own.
> [!NOTE]
> **VPN is still required for the initial Snipe-IT login.** The OAuth flow
> redirects the user's browser to `https://<your-snipeit>/oauth/authorize` for
> sign-in (and SSO bounce), which is VPN-only by definition. Once the user has
> signed in once, subsequent MCP tool calls and refresh-token rotation go
> client backend → DMZ → MCP VM → Snipe-IT entirely server-side, so users can
> keep using the web client from anywhere until the refresh token expires or
> is revoked, at which point a one-time VPN reconnect is needed to re-login.
## MCP Client Configuration
The right configuration depends on whether the server runs in **API-key mode** (stdio,
local, one shared identity) or **OAuth mode** (HTTP, remote, per-user identity).
See the previous section for how the server picks between them.
### Claude Desktop / Claude Code — API-key mode (stdio)
Add to your MCP configuration file:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
**Option A: Install directly from GitHub (no clone required)**
```json
{
"mcpServers": {
"snipeit": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/jameshgordy/snipeit-mcp",
"snipeit-mcp"
],
"env": {
"SNIPEIT_URL": "https://your-snipeit-instance.com",
"SNIPEIT_TOKEN": "your-api-token-here"
}
}
}
}
```
**Option B: Run from a local clone**
```json
{
"mcpServers": {
"snipeit": {
"command": "uv",
"args": [
"--directory",
"/path/to/snipeit-mcp",
"run",
"snipeit-mcp"
],
"env": {
"SNIPEIT_URL": "https://your-snipeit-instance.com",
"SNIPEIT_TOKEN": "your-api-token-here"
}
}
}
}
```
### Claude Desktop / Claude Code — OAuth mode (via `mcp-remote`)
When the server runs in OAuth mode it speaks HTTP, not stdio, so it cannot be
launched directly by Claude Desktop. Use [`mcp-remote`](https://www.npmjs.com/package/mcp-remote)
as a stdio bridge — it handles Dynamic Client Registration, opens the browser
for interactive login, caches the resulting tokens, and refreshes them
transparently. The server must already be running and reachable at the URL below
(e.g. on a VM, behind a reverse proxy, or on `localhost` for dev).
```json
{
"mcpServers": {
"snipeit": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://your-mcp-public-url/mcp"
]
}
}
}
```
> [!NOTE]
> No `SNIPEIT_*` env vars belong here — the server holds them. The first
> connection opens a browser tab for Snipe-IT login (going through your SSO if
> configured); subsequent connections reuse the cached refresh token.
### Claude.ai web (OAuth mode only)
Open https://claude.ai → Settings → **Connectors** → **Add custom connector**.
Paste the public URL of your running MCP server (e.g. `https://your-mcp-public-url/mcp`)
and follow the OAuth prompt.
> [!IMPORTANT]
> Claude.ai web requires the MCP server to be reachable from the public
> internet over HTTPS — `localhost` and unencrypted HTTP do not work here.
> Use the [`mcp-remote` bridge](#claude-desktop--claude-code--oauth-mode-via-mcp-remote)
> instead if you only have a localhost deployment.
### Cursor
Add to your Cursor MCP settings using the same JSON shape as the Claude Desktop
examples above — stdio (API-key) or `mcp-remote` bridge (OAuth) — whichever matches
your server mode.
### MCP Inspector (debugging)
```bash
npx @modelcontextprotocol/inspector
```
Then in the Inspector UI:
- **Transport Type**: `Streamable HTTP`
- **URL**: your server's `/mcp` endpoint
- **Connection Type**: must be **Via Proxy**, not **Direct**
> [!NOTE]
> **Why "Via Proxy"?** The Inspector UI is a static frontend served at
> `http://localhost:6274` and the MCP server lives at a different origin
> (e.g. `http://localhost:8000`). In **Direct** mode the browser tries to talk
> to the MCP server itself, which fails for two compounding reasons: (a)
> FastMCP doesn't emit CORS headers for the Inspector origin, so requests are
> blocked client-side, and (b) the OAuth redirect flow needs server-side state
> the browser-only client can't keep. **Via Proxy** routes traffic through
> Inspector's own backend (at `localhost:6277`), which is same-origin from
> the MCP server's perspective and handles OAuth state correctly.
<!-- separates the two adjacent GH alerts -->
> [!WARNING]
> Leave **Client ID** and **Client Secret** in the OAuth panel **empty** —
> Inspector will perform Dynamic Client Registration with the MCP server.
> Pasting your Snipe-IT (upstream) client_id and secret there is the most
> common misconfiguration; those credentials belong only in the server's
> `.env`, not in any MCP client.
## Available Tools (40 core + 2 optional)
### Asset Tools (8)
| Tool | Description |
|------|-------------|
| `manage_assets` | CRUD operations with bytag/byserial lookup and advanced filtering |
| `asset_operations` | State operations (checkout, checkin, audit, restore) |
| `bulk_asset_operations` | Bulk edit and bulk audit across many assets at once (Snipe-IT v8.7+) |
| `asset_files` | File attachments (upload, list, download, delete) |
| `asset_labels` | Generate printable PDF labels |
| `asset_maintenance` | Full maintenance lifecycle (create, list, get, update, delete, complete) |
| `asset_licenses` | View licenses assigned to an asset |
| `asset_requests` | Submit/cancel checkout requests, list own requests and requestable assets |
### Inventory Tools (5)
| Tool | Description |
|------|-------------|
| `manage_consumables` | CRUD operations for consumables |
| `manage_components` | CRUD operations for components |
| `component_operations` | Checkout/checkin components to assets |
| `manage_accessories` | CRUD operations for accessories |
| `accessory_operations` | Checkout/checkin accessories to users, assets, or locations |
### Kit Tools (1)
| Tool | Description |
|------|-------------|
| `manage_kits` | CRUD for predefined kits plus kit content management (models, licenses, accessories, consumables) |
### User & Organization Tools (6)
| Tool | Description |
|------|-------------|
| `manage_users` | CRUD operations for users (+ restore, me) |
| `user_assets` | Get items checked out to a user (assets, accessories, licenses, consumables, eulas) |
| `user_two_factor` | Reset user two-factor authentication |
| `manage_companies` | CRUD operations for companies |
| `manage_departments` | CRUD operations for departments |
| `manage_groups` | CRUD operations for permission groups |
### Configuration Tools (7)
| Tool | Description |
|------|-------------|
| `manage_categories` | Manage categories for all item types |
| `manage_manufacturers` | Manage manufacturer information |
| `manage_models` | Manage asset models (+ list assets by model) |
| `manage_status_labels` | Manage status labels (+ list assets by status) |
| `manage_locations` | Manage locations (+ list assets/users by location) |
| `manage_suppliers` | Manage supplier information |
| `manage_depreciations` | Manage depreciation schedules |
### Custom Field Tools (2)
| Tool | Description |
|------|-------------|
| `manage_fields` | CRUD + associate/disassociate fields with fieldsets |
| `manage_fieldsets` | CRUD operations for fieldsets (+ field listing, reorder) |
### License Tools (3)
| Tool | Description |
|------|-------------|
| `manage_licenses` | CRUD operations for licenses |
| `license_seats` | Manage license seat assignments |
| `license_files` | Manage license file attachments |
### Reporting & Audit Tools (3)
| Tool | Description |
|------|-------------|
| `activity_reports` | Query activity logs and item history |
| `status_summary` | Get asset counts grouped by status label |
| `audit_tracking` | Track assets due/overdue for audit |
### Import Tools (1)
| Tool | Description |
|------|-------------|
| `manage_imports` | CSV import workflow (upload, then process with type and column mapping) |
### Data Quality Tools (2, optional — require a judgment backend)
| Tool | Description |
|------|-------------|
| `find_duplicates` | Find likely duplicate manufacturers, models, suppliers, locations, categories, companies or departments; pairwise judgments with score, confidence, both records and a merge hint |
| `match_records` | Resolve free-text names to existing record ids (or `none`) with per-option probabilities — for import prep and natural-language asset creation |
### System Administration Tools (4)
| Tool | Description |
|------|-------------|
| `system_info` | Get Snipe-IT version information |
| `manage_backups` | List and download database backups |
| `ldap_operations` | LDAP sync and connection testing |
| `model_files` | Manage file attachments for asset models |
## Usage Examples
### Create an Asset
```json
{
"action": "create",
"asset_data": {
"status_id": 1,
"model_id": 5,
"asset_tag": "LAP-001",
"name": "MacBook Pro 14",
"serial": "C02X12345"
}
}
```
### Create a User
```json
{
"action": "create",
"user_data": {
"first_name": "John",
"last_name": "Doe",
"username": "jdoe",
"email": "jdoe@example.com",
"password": "securepassword",
"password_confirmation": "securepassword",
"department_id": 1
}
}
```
### Get Items Checked Out to User
```json
{
"user_id": 123,
"asset_type": "all"
}
```
### Checkout Component to Asset
```json
{
"action": "checkout",
"component_id": 45,
"checkout_data": {
"assigned_to": 123,
"assigned_qty": 2,
"note": "RAM upgrade"
}
}
```
### Query Activity Logs
```json
{
"action": "list",
"action_type": "checkout",
"limit": 50
}
```
### Create Custom Field
```json
{
"action": "create",
"field_data": {
"name": "MAC Address",
"element": "text",
"format": "MAC"
}
}
```
### Associate Field with Fieldset
```json
{
"action": "associate",
"field_id": 5,
"fieldset_id": 1,
"required": true,
"order": 1
}
```
## Response Format
All tools return structured JSON responses:
**Success (create):**
```json
{
"success": true,
"action": "create",
"asset": {
"id": 123,
"asset_tag": "LAP-001",
"name": "MacBook Pro 14"
}
}
```
**Success (list):**
All list endpoints return pagination metadata:
```json
{
"success": true,
"action": "list",
"count": 3,
"total": 1602,
"limit": 20,
"offset": 0,
"has_more": true,
"assets": [ ... ]
}
```
| Field | Description |
|-------|-------------|
| `count` | Number of items in this page |
| `total` | Total items matching the query |
| `limit` | Page size used |
| `offset` | Starting offset |
| `has_more` | `true` if more pages remain |
**Error:**
```json
{
"success": false,
"error": "Asset not found: Asset with tag LAP-999 not found."
}
```
## Architecture
```
src/snipeit_mcp/
├── __init__.py # Public API re-exports
├── __main__.py # Entry point (snipeit-mcp script)
├── mcp_server.py # FastMCP instance + tool whitelist + optional-tool visibility
├── client.py # SnipeIT API clients
├── judgment.py # Judgment backend config/selection + shared HTTP retry (optional tools)
├── systemone.py # System One protocol backend (TypeSafe Jev, Laya, CLM, …)
├── chat_judge.py # OpenAI-compatible chat backend (Ollama, vLLM, LM Studio, …)
├── config.py # Transport + auth-mode config (OAuth / API key / multi-identity)
├── identity.py # Multi-identity registry (tokens → PATs), ContextVar, validation
├── http_auth.py # Multi-identity HTTP auth (401, /healthz) + audit log + tool policy
├── schemas.py # Pydantic input schemas
└── tools/ # 11 modules grouped by Snipe-IT domain
├── assets.py
├── inventory.py
├── foundational.py
├── licenses.py
├── people.py
├── custom_fields.py
├── reports.py
├── imports.py
├── system.py
└── data_quality.py # find_duplicates / match_records (judgment-backed)
```
Built with:
- **[FastMCP](https://gofastmcp.com)**: Python framework for MCP servers
- **[snipeit-python-api](https://github.com/lfctech/snipeit-python-api)**: Snipe-IT API client
- **[Pydantic](https://docs.pydantic.dev)**: Data validation and type safety
- **Judgment backends** (optional): the [System One protocol](https://docs.typesafe.ai/) (TypeSafe Jev, Laya, CLM, …) or any OpenAI-compatible chat server (Ollama, …) behind the data-quality tools
## Troubleshooting
### Authentication Errors
- Verify your Snipe-IT URL includes the protocol (`https://`)
- Check that your API token is valid and not expired
- Ensure the token has appropriate permissions for the operations
### Connection Errors
- Verify network connectivity to your Snipe-IT instance
- Check for any firewall or proxy restrictions
- Ensure the Snipe-IT instance is running
### Validation Errors
- Check that required fields are provided (e.g., `status_id` and `model_id` for assets)
- Verify that referenced IDs exist (categories, models, locations, etc.)
- Review the tool documentation for required parameters
## License
MIT License. See [LICENSE](LICENSE).
## Contributing
Contributions welcome! Please open an issue or submit a pull request.
Issues and pull requests are generally reviewed on Mondays (unless I'm on vacation), so expect a reply within about a week. New issues get labelled automatically in the meantime.
TDQS
Scored across 40 tools
Tools are mostly separated by resource and operation, with clear distinctions between CRUD, state operations, file attachments, and reporting. A few asset-adjacent tools (manage_assets, asset_operations, bulk_asset_operations, audit_tracking) overlap enough to require careful reading, but descriptions resolve most ambiguity.
The set uses consistent snake_case and a predictable entity-centric pattern, especially manage_<entity> for CRUD and <entity>_operations/_files for specialized actions. Some names deviate from the verb_noun pattern (asset_labels, status_summary, user_assets), but the overall convention remains readable.
40 tools is heavy for an MCP server, even one covering a broad asset-management platform. The multi-action design keeps it from exploding further, and each tool maps to a Snipe-IT resource, but the surface is large enough to increase agent selection burden.
Coverage is broad: CRUD for most Snipe-IT entities, checkout/checkin operations, imports, files, licenses, audits, backups, and LDAP. A notable gap is consumable checkout/checkin operations, since consumables have CRUD but no operational tool analogous to accessories or components.