From e34a2cad111e7325c6469f5909eb96ee3af7204e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Apr 2026 14:00:03 +0200 Subject: [PATCH] docs: add authentication, API reference, and MCP server guides Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/api.md | 642 +++++++++++++++++++++++++++++++++++++++++ docs/authentication.md | 281 ++++++++++++++++++ docs/mcp-server.md | 253 ++++++++++++++++ 3 files changed, 1176 insertions(+) create mode 100644 docs/api.md create mode 100644 docs/authentication.md create mode 100644 docs/mcp-server.md diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..86cfbfe --- /dev/null +++ b/docs/api.md @@ -0,0 +1,642 @@ +# API Reference + +Base URL: `http://localhost:3000` + +**Auth:** GET endpoints are public. POST, PUT, PATCH, and DELETE require authentication via session cookie or `X-API-Key` header. See [authentication.md](authentication.md) for details. + +**Prices** are stored and returned in cents (e.g., `2500` = $25.00). +**Weights** are in grams. +**Timestamps** are unix epoch integers. + +--- + +## Table of Contents + +- [Health](#health) +- [Items](#items) +- [Categories](#categories) +- [Threads](#threads) +- [Setups](#setups) +- [Images](#images) +- [Settings](#settings) +- [Totals](#totals) +- [Auth](#auth) + +--- + +## Health + +### `GET /api/health` + +Returns server status. Always public. + +**Response:** +```json +{ "status": "ok" } +``` + +--- + +## Items + +### `GET /api/items` + +List all items in the collection. + +**Response:** +```json +[ + { + "id": 1, + "name": "Revelate Tangle Frame Bag", + "categoryId": 2, + "weightGrams": 185, + "priceCents": 12000, + "notes": "Medium size, fits 2020 Surly Straggler", + "productUrl": "https://revelatedesigns.com/...", + "imageFilename": "1710000000000-uuid.jpg", + "imageSourceUrl": "https://example.com/image.jpg", + "createdAt": 1710000000, + "updatedAt": 1710000000 + } +] +``` + +--- + +### `GET /api/items/:id` + +Get a single item by ID. + +**Response:** item object (see above), or `404 { "error": "Item not found" }`. + +--- + +### `POST /api/items` + +Create a new item. Auth required. + +**Request:** +```json +{ + "name": "Revelate Tangle Frame Bag", + "categoryId": 2, + "weightGrams": 185, + "priceCents": 12000, + "notes": "Medium size", + "productUrl": "https://revelatedesigns.com/...", + "imageFilename": "1710000000000-uuid.jpg", + "imageSourceUrl": "https://example.com/image.jpg" +} +``` + +| Field | Type | Required | Description | +|-----------------|---------|----------|--------------------------------------| +| `name` | string | yes | Item name | +| `categoryId` | integer | yes | ID of an existing category | +| `weightGrams` | number | no | Weight in grams (non-negative) | +| `priceCents` | integer | no | Price in cents (non-negative) | +| `notes` | string | no | Free-text notes | +| `productUrl` | string | no | URL to product page | +| `imageFilename` | string | no | Filename from a prior image upload | +| `imageSourceUrl`| string | no | Original URL the image came from | + +**Response:** `201` — created item object. + +--- + +### `PUT /api/items/:id` + +Update an existing item. All fields are optional. Auth required. + +**Request:** same fields as POST, all optional. + +**Response:** updated item object, or `404`. + +--- + +### `DELETE /api/items/:id` + +Delete an item and clean up its image file if one exists. Auth required. + +**Response:** +```json +{ "success": true } +``` + +Returns `404` if item not found. + +--- + +## Categories + +### `GET /api/categories` + +List all categories. + +**Response:** +```json +[ + { "id": 1, "name": "Uncategorized", "icon": "package" }, + { "id": 2, "name": "Bags", "icon": "backpack" } +] +``` + +--- + +### `POST /api/categories` + +Create a new category. Auth required. + +**Request:** +```json +{ "name": "Bags", "icon": "backpack" } +``` + +| Field | Type | Required | Description | +|--------|--------|----------|--------------------------------------| +| `name` | string | yes | Category name | +| `icon` | string | no | Icon name (defaults to `"package"`) | + +**Response:** `201` — created category object. + +--- + +### `PUT /api/categories/:id` + +Update a category. Auth required. + +**Request:** +```json +{ "name": "Carry", "icon": "bag" } +``` + +**Response:** updated category object, or `404`. + +--- + +### `DELETE /api/categories/:id` + +Delete a category. Auth required. + +**Response:** +```json +{ "success": true } +``` + +--- + +## Threads + +Research threads track multiple candidate options for a single gear slot before committing to a purchase. + +### `GET /api/threads` + +List threads. By default only active (unresolved) threads are returned. + +**Query parameters:** + +| Parameter | Type | Default | Description | +|-------------------|---------|---------|------------------------------------| +| `includeResolved` | boolean | `false` | Set to `true` to include resolved threads | + +**Example:** `GET /api/threads?includeResolved=true` + +**Response:** +```json +[ + { + "id": 1, + "name": "Handlebar bag", + "categoryId": 2, + "status": "active", + "resolvedCandidateId": null, + "createdAt": 1710000000, + "updatedAt": 1710000000 + } +] +``` + +--- + +### `POST /api/threads` + +Create a new research thread. Auth required. + +**Request:** +```json +{ "name": "Handlebar bag", "categoryId": 2 } +``` + +**Response:** `201` — created thread object. + +--- + +### `GET /api/threads/:id` + +Get a thread with all its candidates. + +**Response:** +```json +{ + "id": 1, + "name": "Handlebar bag", + "categoryId": 2, + "status": "active", + "resolvedCandidateId": null, + "candidates": [ + { + "id": 10, + "threadId": 1, + "name": "Revelate Sweetroll", + "categoryId": 2, + "weightGrams": 290, + "priceCents": 13500, + "notes": "16L, fits most drop bars", + "productUrl": "https://revelatedesigns.com/...", + "imageFilename": null, + "imageSourceUrl": null, + "status": "researching", + "pros": "Waterproof, large capacity", + "cons": "Pricey", + "sortOrder": 0, + "createdAt": 1710000000, + "updatedAt": 1710000000 + } + ] +} +``` + +Returns `404` if thread not found. + +--- + +### `PUT /api/threads/:id` + +Update a thread's name or category. Auth required. + +**Request:** +```json +{ "name": "Bar bag", "categoryId": 2 } +``` + +**Response:** updated thread object, or `404`. + +--- + +### `DELETE /api/threads/:id` + +Delete a thread and all its candidates. Cleans up any candidate image files. Auth required. + +**Response:** +```json +{ "success": true } +``` + +--- + +### `POST /api/threads/:id/candidates` + +Add a candidate to a thread. Auth required. + +**Request:** +```json +{ + "name": "Revelate Sweetroll", + "categoryId": 2, + "weightGrams": 290, + "priceCents": 13500, + "notes": "16L capacity", + "productUrl": "https://revelatedesigns.com/...", + "imageFilename": "1710000000000-uuid.jpg", + "imageSourceUrl": "https://example.com/image.jpg", + "status": "researching", + "pros": "Waterproof, large capacity", + "cons": "Expensive" +} +``` + +| Field | Type | Required | Description | +|-----------------|---------|----------|----------------------------------------------------| +| `name` | string | yes | Candidate name | +| `categoryId` | integer | yes | Category ID | +| `weightGrams` | number | no | Weight in grams | +| `priceCents` | integer | no | Price in cents | +| `notes` | string | no | Notes | +| `productUrl` | string | no | Product URL | +| `imageFilename` | string | no | Filename from a prior image upload | +| `imageSourceUrl`| string | no | Original image URL | +| `status` | string | no | `"researching"`, `"ordered"`, or `"arrived"` | +| `pros` | string | no | Pros of this option | +| `cons` | string | no | Cons of this option | + +**Response:** `201` — created candidate object. Returns `404` if thread not found. + +--- + +### `PUT /api/threads/:threadId/candidates/:candidateId` + +Update a candidate. All fields optional. Auth required. + +**Request:** same fields as POST, all optional. + +**Response:** updated candidate object, or `404`. + +--- + +### `DELETE /api/threads/:threadId/candidates/:candidateId` + +Delete a candidate and clean up its image. Auth required. + +**Response:** +```json +{ "success": true } +``` + +--- + +### `PATCH /api/threads/:id/candidates/reorder` + +Reorder candidates within a thread. Auth required. + +**Request:** +```json +{ "orderedIds": [12, 10, 11] } +``` + +`orderedIds` is an array of candidate IDs in the desired display order. All IDs must belong to the thread. + +**Response:** +```json +{ "success": true } +``` + +Returns `400` with an error message if validation fails. + +--- + +### `POST /api/threads/:id/resolve` + +Resolve a thread by selecting the winning candidate. Auth required. + +The winning candidate's data is copied into a new item in the collection. The thread status is set to `"resolved"` and `resolvedCandidateId` is set. + +**Request:** +```json +{ "candidateId": 10 } +``` + +**Response:** +```json +{ + "success": true, + "item": { + "id": 42, + "name": "Revelate Sweetroll", + "categoryId": 2, + "weightGrams": 290, + "priceCents": 13500 + } +} +``` + +Returns `400` if the candidate does not belong to the thread or another error occurs. + +--- + +## Setups + +Setups are named collections of items (e.g., "Bikepacking weekend", "Commute loadout"). + +### `GET /api/setups` + +List all setups with item counts and weight/cost totals. + +**Response:** +```json +[ + { + "id": 1, + "name": "Bikepacking weekend", + "itemCount": 12, + "totalWeightGrams": 4800, + "totalPriceCents": 285000, + "createdAt": 1710000000, + "updatedAt": 1710000000 + } +] +``` + +--- + +### `POST /api/setups` + +Create a new setup. Auth required. + +**Request:** +```json +{ "name": "Bikepacking weekend" } +``` + +**Response:** `201` — created setup object. + +--- + +### `GET /api/setups/:id` + +Get a setup with its full item list. + +**Response:** +```json +{ + "id": 1, + "name": "Bikepacking weekend", + "items": [ + { + "id": 1, + "name": "Revelate Tangle Frame Bag", + "weightGrams": 185, + "priceCents": 12000, + "categoryId": 2, + "classification": "base" + } + ], + "totalWeightGrams": 185, + "totalPriceCents": 12000 +} +``` + +Returns `404` if setup not found. + +--- + +### `PUT /api/setups/:id` + +Update a setup's name. Auth required. + +**Request:** +```json +{ "name": "Bikepacking overnighter" } +``` + +**Response:** updated setup object, or `404`. + +--- + +### `PUT /api/setups/:id/items` + +Replace all items in a setup atomically. Deletes all existing setup-item associations and re-inserts with the provided IDs. Auth required. + +**Request:** +```json +{ "itemIds": [1, 5, 12, 17] } +``` + +Pass an empty array to remove all items from the setup. + +**Response:** +```json +{ "success": true } +``` + +--- + +### `DELETE /api/setups/:id` + +Delete a setup. Does not delete the items themselves. Auth required. + +**Response:** +```json +{ "success": true } +``` + +--- + +## Images + +### `POST /api/images` + +Upload an image file. Auth required. + +**Request:** `multipart/form-data` with an `image` field. + +``` +POST /api/images +Content-Type: multipart/form-data; boundary=... + +--boundary +Content-Disposition: form-data; name="image"; filename="bag.jpg" +Content-Type: image/jpeg + + +--boundary-- +``` + +Constraints: +- Accepted types: `image/jpeg`, `image/png`, `image/webp` +- Maximum size: 5MB +- Files are saved to `./uploads/` with a UUID-based filename + +**Response:** `201` +```json +{ "filename": "1710000000000-550e8400-e29b-41d4-a716-446655440000.jpg" } +``` + +Use the returned `filename` as `imageFilename` when creating or updating items or candidates. + +--- + +### `POST /api/images/from-url` + +Fetch an image from a remote URL and save it locally. Auth required. + +**Request:** +```json +{ "url": "https://example.com/product-image.jpg" } +``` + +**Response:** `201` +```json +{ + "filename": "1710000000000-uuid.jpg", + "sourceUrl": "https://example.com/product-image.jpg" +} +``` + +Returns `400` for invalid URLs, unsupported content types, files over 5MB, or non-200 HTTP responses. + +--- + +## Settings + +Key-value store for application settings. + +### `GET /api/settings/:key` + +Get a setting by key. Public. + +**Response:** +```json +{ "key": "collectionName", "value": "My Bikepacking Gear" } +``` + +Returns `404` if the key does not exist. + +--- + +### `PUT /api/settings/:key` + +Create or update a setting. Auth required. + +**Request:** +```json +{ "value": "My Bikepacking Gear" } +``` + +**Response:** updated setting object. + +--- + +## Totals + +### `GET /api/totals` + +Global and per-category weight and cost totals. Computed from current item data. Public. + +**Response:** +```json +{ + "global": { + "totalWeightGrams": 12500, + "totalPriceCents": 450000, + "itemCount": 34 + }, + "categories": [ + { + "categoryId": 2, + "name": "Bags", + "icon": "backpack", + "totalWeightGrams": 1200, + "totalPriceCents": 78000, + "itemCount": 5 + } + ] +} +``` + +--- + +## Auth + +Authentication endpoints are documented in [authentication.md](authentication.md). + +| Method | Path | Auth | Description | +|--------|-------------------------|----------|---------------------------| +| GET | `/api/auth/me` | public | Current session state | +| POST | `/api/auth/setup` | public | Create first admin user | +| POST | `/api/auth/login` | public | Log in, receive cookie | +| POST | `/api/auth/logout` | public | Clear session | +| PUT | `/api/auth/password` | session | Change password | +| GET | `/api/auth/keys` | required | List API keys | +| POST | `/api/auth/keys` | required | Create API key | +| DELETE | `/api/auth/keys/:id` | required | Revoke API key | diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..a11b316 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,281 @@ +# Authentication + +GearBox uses a public-read, authenticated-write model. All GET endpoints are publicly accessible with no credentials required. Any request that modifies data (POST, PUT, PATCH, DELETE) requires authentication. + +This is a single-user app. There is exactly one admin account. + +## Table of Contents + +- [First-Time Setup](#first-time-setup) +- [Web UI Authentication](#web-ui-authentication) +- [API Keys](#api-keys) +- [Auth Middleware Behavior](#auth-middleware-behavior) +- [Auth API Reference](#auth-api-reference) +- [Frontend Behavior](#frontend-behavior) + +--- + +## First-Time Setup + +When no users exist, all write endpoints return `403` with `{ "error": "setup_required" }`. To create the admin account, visit `/login` in the browser and complete the setup form, or call the setup endpoint directly: + +```http +POST /api/auth/setup +Content-Type: application/json + +{ + "username": "admin", + "password": "yourpassword" +} +``` + +Requirements: +- `username`: any non-empty string +- `password`: minimum 6 characters + +This endpoint only works when no users exist. Subsequent calls return `403 { "error": "Setup already completed" }`. + +On success, a session cookie is set and `201` is returned: + +```json +{ "username": "admin" } +``` + +--- + +## Web UI Authentication + +Sessions use an `httpOnly` cookie named `gearbox_session`. + +| Property | Value | +|------------|--------------------| +| Cookie name | `gearbox_session` | +| httpOnly | true | +| sameSite | Lax | +| path | / | +| Max age | 30 days | + +The session expiry is **automatically refreshed** on each authenticated request. As long as the app is used at least once every 30 days, the session stays active. + +Passwords are hashed with **argon2** via `Bun.password`. + +### Changing Your Password + +Requires an active session cookie. + +```http +PUT /api/auth/password +Content-Type: application/json + +{ + "currentPassword": "oldpassword", + "newPassword": "newpassword" +} +``` + +--- + +## API Keys + +API keys are intended for programmatic access (scripts, MCP clients, integrations). They are managed under **Settings > API Keys** in the web UI, or via the API endpoints listed below. + +### Key behavior + +- Keys are shown **once** at creation time. Store them securely. +- Keys are stored as an argon2 hash. Only the 8-character prefix is stored in plaintext for display and lookup purposes. +- Pass the key via the `X-API-Key` request header on any write request. + +```http +POST /api/items +X-API-Key: gbk_a1b2c3d4... +Content-Type: application/json + +{ "name": "Revelate Tangle", "categoryId": 2 } +``` + +If both a session cookie and an `X-API-Key` header are present, the API key is checked first. + +--- + +## Auth Middleware Behavior + +The middleware applied to `/api/*` (excluding `/api/auth/*`) follows these rules: + +1. `GET` requests — always allowed, no auth check. +2. No users exist — returns `403 { "error": "setup_required" }`. +3. `X-API-Key` header present — verified against stored hashes; `401` on failure. +4. `gearbox_session` cookie present — verified against sessions table; refreshed on success; `401` on failure. +5. Neither credential present — returns `401 { "error": "Authentication required" }`. + +The `/api/auth/*` routes handle their own auth logic and are excluded from the global middleware. + +--- + +## Auth API Reference + +### `GET /api/auth/me` + +Returns the current session state. Always public. + +**Response when logged in:** +```json +{ + "user": { "id": 1 }, + "setupRequired": false +} +``` + +**Response when logged out, setup complete:** +```json +{ + "user": null, + "setupRequired": false +} +``` + +**Response when no users exist:** +```json +{ + "user": null, + "setupRequired": true +} +``` + +--- + +### `POST /api/auth/setup` + +Create the first admin account. Only works when no users exist. + +**Request:** +```json +{ + "username": "admin", + "password": "yourpassword" +} +``` + +**Response:** `201` +```json +{ "username": "admin" } +``` + +Sets `gearbox_session` cookie. + +--- + +### `POST /api/auth/login` + +Log in with username and password. + +**Request:** +```json +{ + "username": "admin", + "password": "yourpassword" +} +``` + +**Response:** `200` +```json +{ "username": "admin" } +``` + +Sets `gearbox_session` cookie. Returns `401` on invalid credentials. + +--- + +### `POST /api/auth/logout` + +Clear the current session. No request body needed. + +**Response:** +```json +{ "ok": true } +``` + +Clears the `gearbox_session` cookie and deletes the session from the database. + +--- + +### `PUT /api/auth/password` + +Change the admin password. Requires an active session cookie (not API key). + +**Request:** +```json +{ + "currentPassword": "oldpassword", + "newPassword": "newpassword" +} +``` + +**Response:** +```json +{ "ok": true } +``` + +Returns `401` if `currentPassword` is incorrect. + +--- + +### `GET /api/auth/keys` + +List all API keys. Returns name, prefix, and creation timestamp — never the full key. + +Requires auth. + +**Response:** +```json +[ + { + "id": 1, + "name": "Claude Code", + "prefix": "gbk_a1b2", + "createdAt": "2025-03-01T10:00:00.000Z" + } +] +``` + +--- + +### `POST /api/auth/keys` + +Create a new API key. The full key is returned **once** and cannot be retrieved again. + +Requires auth. + +**Request:** +```json +{ "name": "Claude Code" } +``` + +**Response:** `201` +```json +{ + "id": 1, + "name": "Claude Code", + "key": "gbk_a1b2c3d4e5f6g7h8i9j0...", + "prefix": "gbk_a1b2" +} +``` + +--- + +### `DELETE /api/auth/keys/:id` + +Revoke an API key by ID. Requires auth. + +**Response:** +```json +{ "ok": true } +``` + +--- + +## Frontend Behavior + +- A login button is shown in the top-right corner of the UI (Gitea-style). +- The floating action button (FAB) for adding items is hidden when not logged in. +- Edit and delete actions on items, threads, and setups require auth. Unauthenticated users see read-only views. +- When `setupRequired` is true, the UI redirects to the setup flow. diff --git a/docs/mcp-server.md b/docs/mcp-server.md new file mode 100644 index 0000000..ccdacb1 --- /dev/null +++ b/docs/mcp-server.md @@ -0,0 +1,253 @@ +# MCP Server + +GearBox includes a built-in MCP (Model Context Protocol) server that exposes your gear collection to AI assistants. It runs inside the Hono process — no separate service is needed. + +The MCP server implements the [Streamable HTTP transport](https://modelcontextprotocol.io/specification) from `@modelcontextprotocol/sdk` and is mounted at `/mcp`. + +--- + +## Table of Contents + +- [Enabling and Disabling](#enabling-and-disabling) +- [Authentication](#authentication) +- [Connecting an AI Client](#connecting-an-ai-client) + - [Claude Code](#claude-code) + - [Claude Desktop](#claude-desktop) +- [Tools](#tools) +- [Resources](#resources) +- [Recommended Workflow](#recommended-workflow) +- [Example Session](#example-session) +- [Implementation Structure](#implementation-structure) + +--- + +## Enabling and Disabling + +The MCP server is **enabled by default**. To disable it, set the environment variable: + +```bash +GEARBOX_MCP=false bun run dev:server +``` + +Or in your environment file: + +```bash +GEARBOX_MCP=false +``` + +When disabled, the `/mcp` route is not registered and any requests to it return 404. + +--- + +## Authentication + +The MCP server requires an API key when the app has been set up (i.e., at least one user exists). Pass the key via the `X-API-Key` header in your MCP client configuration. + +To create an API key: +1. Log in to the web UI. +2. Go to **Settings > API Keys**. +3. Click **New Key**, give it a name, and copy the full key shown. + +The full key is only shown once. Store it in your client configuration immediately. + +See [authentication.md](authentication.md) for full API key documentation. + +--- + +## Connecting an AI Client + +### Claude Code + +Add the MCP server to `.claude/settings.json` in your project or globally at `~/.claude/settings.json`: + +```json +{ + "mcpServers": { + "gearbox": { + "type": "streamable-http", + "url": "http://localhost:3000/mcp", + "headers": { + "X-API-Key": "your-api-key-here" + } + } + } +} +``` + +### Claude Desktop + +Add the server to `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "gearbox": { + "type": "streamable-http", + "url": "http://localhost:3000/mcp", + "headers": { + "X-API-Key": "your-api-key-here" + } + } + } +} +``` + +On macOS, the config file is at `~/Library/Application Support/Claude/claude_desktop_config.json`. + +After updating the config, restart Claude Desktop (or reload the window in Claude Code) for the server to connect. + +--- + +## Tools + +The MCP server exposes 18 tools across five categories. + +### Items + +| Tool | Description | +|----------------|------------------------------------------------------------------------------------------------------------| +| `list_items` | List all items in the gear collection, optionally filtered by `categoryId`. | +| `get_item` | Get a single item by ID with all details. | +| `create_item` | Add a new item directly to the collection. Use for items already decided on; use `create_thread` for research. | +| `update_item` | Update an existing item's fields. | +| `delete_item` | Delete an item from the collection by ID. | + +### Categories + +| Tool | Description | +|-------------------|--------------------------------------| +| `list_categories` | List all gear categories. | +| `create_category` | Create a new gear category. | + +### Threads + +| Tool | Description | +|------------------|------------------------------------------------------------------------------------------------------| +| `list_threads` | List research threads. Pass `includeResolved: true` to include resolved threads. | +| `get_thread` | Get a thread with all its candidates for detailed comparison. | +| `create_thread` | Start a new research thread for a gear slot. Preferred entry point for gear research. | +| `resolve_thread` | Resolve a thread by picking the winning candidate. Automatically creates a new item in the collection. | + +### Candidates + +| Tool | Description | +|--------------------|-----------------------------------------------------------------------| +| `add_candidate` | Add a candidate option to a research thread for comparison. | +| `update_candidate` | Update a candidate's details (name, price, pros, cons, status, etc.).| +| `remove_candidate` | Remove a candidate from a research thread. | + +### Setups + +| Tool | Description | +|-----------------|-------------------------------------------------------------------------------------------------------| +| `list_setups` | List all gear setups with item counts and weight/cost totals. | +| `get_setup` | Get a setup with all its items and details. | +| `create_setup` | Create a new gear setup (e.g., "Bikepacking weekend"). | +| `update_setup` | Update a setup's name and/or replace its item list by passing `itemIds`. | + +### Images + +| Tool | Description | +|-------------------------|--------------------------------------------------------------------------------------------------| +| `upload_image_from_url` | Fetch an image from a URL and save it locally. Returns a `filename` to use with items or candidates. | + +--- + +## Resources + +### `gearbox://collection/summary` + +Returns a JSON overview of the entire gear collection, including: +- Total item count, weight, and cost +- Per-category breakdowns +- Active research thread count + +Read this resource first to orient yourself before running queries or making changes. + +--- + +## Recommended Workflow + +Tool descriptions are written to guide AI assistants toward the research thread workflow rather than creating items directly. The intended flow for evaluating a new piece of gear is: + +1. **`create_thread`** — open a research thread for the gear slot (e.g., "Handlebar bag") +2. **`add_candidate`** — add options with prices, weights, pros, and cons +3. **`get_thread`** — review all candidates side by side +4. **`resolve_thread`** — pick the winner; it becomes a new item in the collection automatically + +Use `create_item` directly only when you already know exactly what you're adding and no research is needed. + +--- + +## Example Session + +The following illustrates a typical assistant interaction via the MCP server. + +**User:** "I need a new handlebar bag. Can you research some options and add them?" + +**Assistant actions:** + +``` +1. list_categories + -> finds "Bags" category (id: 2) + +2. create_thread { name: "Handlebar bag", categoryId: 2 } + -> thread id: 7 + +3. upload_image_from_url { url: "https://revelatedesigns.com/sweetroll.jpg" } + -> filename: "1710000000000-uuid.jpg" + +4. add_candidate { + threadId: 7, + name: "Revelate Sweetroll", + categoryId: 2, + weightGrams: 290, + priceCents: 13500, + productUrl: "https://revelatedesigns.com/...", + imageFilename: "1710000000000-uuid.jpg", + pros: "Waterproof, 16L, fits most drop bars", + cons: "Expensive, bulky profile" + } + +5. add_candidate { + threadId: 7, + name: "Apidura Backcountry Handlebar Pack", + categoryId: 2, + weightGrams: 230, + priceCents: 11000, + pros: "Lighter, packable, good attachment system", + cons: "Smaller capacity" + } + +6. add_candidate { ... } + +7. get_thread { id: 7 } + -> presents comparison to user + +8. resolve_thread { threadId: 7, candidateId: 14 } + -> Revelate Sweetroll added to collection as item id: 42 +``` + +--- + +## Implementation Structure + +The MCP server source lives under `src/server/mcp/`: + +``` +src/server/mcp/ + index.ts # Server factory, transport handling, auth middleware, Hono route registration + tools/ + items.ts # list_items, get_item, create_item, update_item, delete_item + categories.ts # list_categories, create_category + threads.ts # list_threads, get_thread, create_thread, resolve_thread, + # add_candidate, update_candidate, remove_candidate + setups.ts # list_setups, get_setup, create_setup, update_setup + images.ts # upload_image_from_url + resources/ + collection.ts # gearbox://collection/summary resource handler +``` + +Each tool file exports a `*ToolDefinitions` array (name, description, inputSchema) and a `register*Tools(db)` factory function that returns handler implementations. The server in `index.ts` iterates over definitions and registers each handler with the MCP `McpServer` instance. + +Session management uses the `WebStandardStreamableHTTPServerTransport` with UUID-based session IDs tracked in an in-memory `Map`. Sessions are cleaned up when the transport closes.