A single static Go binary that exposes the Wardrowbe wardrobe API as tools for Claude. Three capabilities make Claude a first-class part of the wardrobe:
- Image view — returns garment photos to Claude as MCP image content, so Claude's own vision tags and styles them instead of a small in-cluster model.
- Tag / description write-back — lets Claude save accurate attributes back to Wardrowbe, so it can correct what the auto-tagger got wrong.
- Item creation — lets Claude add a garment from an image:
wardrowbe_create_item_from_urlfetches a public image URL (SSRF-guarded), andwardrowbe_create_item_from_base64takes an inline image for local files. The backend stores and auto-tags it; Claude can then refine the tags via write-back.
It runs over Streamable HTTP (or stdio) and is designed to sit in a homelab k3s cluster behind a Cloudflare Access MCP portal, used from Claude Desktop, Mobile, or Code. Current version: 1.0.0.
35 tools covering the wardrobe API, all prefixed wardrowbe_ so they stay unambiguous
in MCP hosts that aggregate several servers: browsing and analytics
(wardrowbe_list_items, wardrowbe_get_item, wardrowbe_get_wardrobe_summary, …),
the external-tagging queue (wardrowbe_list_untagged_items, wardrowbe_retag_item),
wear/wash/archive lifecycle, outfit suggestion and feedback, the image-view tools
(wardrowbe_get_item_image, wardrowbe_get_outfit_images, wardrowbe_download_image), write-back
(wardrowbe_update_item, wardrowbe_set_item_tags, wardrowbe_set_item_description),
and creation (wardrowbe_create_outfit, wardrowbe_create_item_from_url,
wardrowbe_create_item_from_base64, wardrowbe_delete_outfit).
Each tool maps to a Wardrowbe backend endpoint; the definitions live in
internal/mcpserver/tools_*.go.
| Endpoint | Auth | Purpose |
|---|---|---|
GET / |
none | Liveness — static 200, no backend dependency. |
GET /readyz |
none | Readiness — pings the backend (3s); 503 when it's down. |
POST /mcp |
Bearer | The MCP endpoint (Streamable HTTP). |
/mcp is hardened for public exposure: a static bearer gate (constant-time, emits
RFC 9728 WWW-Authenticate on 401), an inbound body-size cap, and panic
recovery. Backend
error bodies are logged server-side only — never surfaced to the caller. Wire a k8s
readinessProbe to /readyz and livenessProbe to /.
Pull a pinned, prebuilt image (multi-arch, distroless, non-root) — the image and Helm chart are published public on GHCR, so no auth is needed to pull them:
docker run --rm -p 8080:8080 -e MCP_API_KEY="$MCP_API_KEY" \
ghcr.io/jansitarski/wardrowbe-mcp:1.0.0 \
--transport http --host 0.0.0.0 --port 8080 \
--wardrowbe-url http://backend.wardrowbe.svc.cluster.local:8000 \
--auth dev --external-id <web-user-external-id> --external-email <real-email>Or download a prebuilt static binary for your architecture (amd64, arm64, or
armv7) from the latest release
— verify it against the published SHA256SUMS.txt:
curl -fL -o wardrowbe-mcp \
https://github.com/jansitarski/wardrowbe-mcp/releases/download/v1.0.0/wardrowbe-mcp_1.0.0_linux_amd64
chmod +x wardrowbe-mcpOr build from source (requires Go 1.25.11+, the version pinned in go.mod):
go build -o wardrowbe-mcp ./cmd/wardrowbe-mcp
# Pass the key via the environment, not argv — it would show up in `ps` output.
MCP_API_KEY="$MCP_API_KEY" ./wardrowbe-mcp \
--transport http --host 0.0.0.0 --port 8080 \
--wardrowbe-url http://backend.wardrowbe.svc.cluster.local:8000 \
--auth dev --external-id <web-user-external-id> --external-email <real-email>Or deploy to Kubernetes with the Helm chart (published as an OCI artifact per release):
helm install wardrowbe-mcp \
oci://ghcr.io/jansitarski/charts/wardrowbe-mcp --version 1.0.0 \
-n wardrowbe --create-namespace \
--set config.wardrowbeUrl=http://backend.wardrowbe.svc.cluster.local:8000 \
--set apiKey.value="$MCP_API_KEY"See charts/wardrowbe-mcp/ for all values and a
Flux HelmRelease example.
Connect from Claude Code:
claude mcp add --transport http wardrowbe <url> --header "Authorization: Bearer $MCP_API_KEY"Every flag has a matching MCP_* (or WARDROWBE_URL) environment variable; flags win.
The most-used ones:
| Flag | Default | Purpose |
|---|---|---|
--transport |
http |
http (Streamable) or stdio. |
--wardrowbe-url |
— | Backend base URL (no /api/v1). Required. |
--api-key |
— | Incoming bearer key; required for http. Prefer MCP_API_KEY env over the flag (argv is visible in ps). |
--auth |
dev |
dev or oidc. |
--external-id / --external-email |
— | Dev identity sent to /auth/sync. |
--oidc-issuer-url / --oidc-client-id |
— | OIDC issuer + client (required with --oidc-refresh-token; unused with a static --oidc-id-token). |
--oidc-refresh-token |
— | Enables the refresh_token grant: a fresh id_token is minted per sync. |
--oidc-refresh-token-file |
— | Persists+reloads the rotated refresh token across restarts (see below). |
--oidc-id-token |
— | Static id_token used when no refresh token is configured. |
--oidc-token-endpoint |
— | Optional https token-endpoint override (skips OIDC discovery). |
--max-body-mb |
40 |
Inbound /mcp body cap. |
--stateless |
true |
Stateless Streamable HTTP: every POST is self-contained, no server-side MCP sessions (survives restarts/idle proxies). Set false to restore stateful sessions. |
--portal-resource-url |
— | Emits the RFC 9728 resource_metadata on 401. |
In oidc mode the raw id_token is forwarded in the /auth/sync body so the
backend validates it against the issuer's JWKS. Supply either a
--oidc-refresh-token (the durable path — the server refreshes the token on
every sync) or a static --oidc-id-token (the fallback for issuers that do
not issue refresh tokens; it expires and is not renewed).
Rotating refresh tokens. Some IdPs (e.g. Cloudflare Access) rotate the
refresh token on every use and invalidate the previous one. The server tracks
the rotation in memory, but a restart would then fall back to the now-dead
configured seed. Set --oidc-refresh-token-file (MCP_OIDC_REFRESH_TOKEN_FILE)
to a path on a persistent volume: each rotation is written there and the latest
is reloaded on startup, so restarts survive. The configured --oidc-refresh-token
is only the initial seed used until the first rotation populates the file.
Run wardrowbe-mcp --help for the complete flag list, including the image and OIDC
options. Every flag also has an MCP_* (or WARDROWBE_URL) environment variable;
see .env.example for the full list. Secrets (MCP_API_KEY,
MCP_OIDC_CLIENT_SECRET, MCP_OIDC_REFRESH_TOKEN, MCP_OIDC_ID_TOKEN) are never
echoed in --help output or flag-error usage text.
| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized on /mcp |
Missing/incorrect bearer | Send Authorization: Bearer $MCP_API_KEY; confirm the value matches --api-key/MCP_API_KEY. |
503 on /readyz |
Backend unreachable | Check --wardrowbe-url and that the backend is up; /readyz pings it. / (liveness) stays 200 regardless. |
413 / request rejected |
Body exceeds cap | Raise --max-body-mb (default 40) for large base64 uploads. |
wardrowbe_create_item_from_url refuses a URL |
SSRF guard / non-public host | The URL must be http(s) and resolve to a public IP; private/loopback/link-local/multicast/NAT64 targets are blocked. |
OIDC refresh fails with invalid_grant |
Refresh token expired/rotated out | Issue a fresh refresh token. The server follows rotation automatically while running, but the rotated token is held in memory only — after a restart it resumes from the configured token, which a rotation-enforcing IdP may have invalidated. Re-issue and update MCP_OIDC_REFRESH_TOKEN whenever this happens; with such IdPs also run a single replica (a shared refresh token across replicas trips reuse detection). |
Intermittent MCP server connection lost from the connector (retry succeeds) |
Stale stream/session between proxy hops | The server heartbeats standing streams every 25s and runs stateless by default, which resolves this. If you set --stateless=false, note that sessions live in pod memory: every restart invalidates them and the connector's next call fails once. |
| Startup log warns about dev auth | --auth dev on http |
Expected for a single user; set --auth oidc for real per-user identity. |
| Process exits at startup with "invalid …" | Bad flag/env value | The config is validated up front — the message names the offending flag/env var. |
make build # static binary
make test # go test -race ./...
make vet
make docker # distroless, non-root imageContributions: see CONTRIBUTING.md. Security reports: docs/SECURITY.md.
docs/deployment.md— running the server against a Wardrowbe backend: reference topology, backend dev auth, identity (--external-id/--external-email), the AI backend, and connecting Claude.docs/connecting-claude-via-cloudflare.md— exposing/mcpto Claude's native connectors through a Cloudflare tunnel + Access MCP portal: the required configuration and the client options.charts/wardrowbe-mcp/— the Helm chart: installable values, the API-key options, and a FluxHelmReleaseexample.skills/wardrowbe-image-upload/— a Claude Code skill for bulk-importing garment photos and giving them accurate tags with Claude's own vision instead of the backend's auto-tagger.
Derived from saya6k/mcp-wardrowbe (MIT), reimplemented in Go.
MIT.