docs: add design specs for image URL fetching, auth, and MCP server

Three independent feature specs covering:
- API endpoint for fetching images from URLs with local storage
- Public-read/authenticated-write auth with sessions and API keys
- Built-in MCP server for Claude Code/Desktop integration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 12:53:51 +02:00
parent 32d6babf24
commit dde2fc241d
3 changed files with 378 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
# Authentication — Design Spec
## Overview
Add authentication to GearBox with a public-read, authenticated-write model. Web UI uses cookie-based sessions. Programmatic access (MCP server, scripts) uses API keys. Single-user app — one admin account, created on first setup.
## Database Schema
### `users` table
```typescript
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
username: text("username").notNull().unique(),
passwordHash: text("password_hash").notNull(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});
```
### `sessions` table
```typescript
export const sessions = sqliteTable("sessions", {
id: text("id").primaryKey(), // random token
userId: integer("user_id").notNull().references(() => users.id),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
});
```
### `apiKeys` table
```typescript
export const apiKeys = sqliteTable("api_keys", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
keyHash: text("key_hash").notNull(),
keyPrefix: text("key_prefix").notNull(), // first 8 chars for identification
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});
```
## Password Hashing
Use `Bun.password.hash()` and `Bun.password.verify()` with argon2 (Bun's default). No external dependencies needed.
## Auth Middleware
Hono middleware applied to all write endpoints (POST, PUT, DELETE):
1. Check for `X-API-Key` header — if present, hash and compare against `api_keys` table
2. Check for session cookie (`gearbox_session`) — if present, look up in `sessions` table, verify not expired
3. If neither is valid, return `401 Unauthorized`
GET endpoints remain public — no middleware applied.
**Exempt from auth:** `/api/auth/login`, `/api/auth/setup`, and `/api/auth/me` (GET) are not protected by the write middleware.
**Before setup:** If no user account exists yet, write endpoints return `403` with `{ error: "setup_required" }` so the frontend can prompt account creation.
## API Endpoints
### Auth routes (`/api/auth`)
- `POST /api/auth/login` — accepts `{ username, password }`, creates session, sets cookie
- `POST /api/auth/logout` — clears session cookie, deletes session record
- `GET /api/auth/me` — returns current user info if authenticated, or `null`
- `POST /api/auth/setup` — initial account creation (only works if no users exist)
- `PUT /api/auth/password` — change password (requires current password)
### API key routes (`/api/auth/keys`) — all authenticated
- `GET /api/auth/keys` — list API keys (name, prefix, createdAt — never the full key)
- `POST /api/auth/keys` — create new key, returns the full key once
- `DELETE /api/auth/keys/:id` — revoke a key
## Session Management
- Session token: 32-byte random hex string
- Cookie: `gearbox_session`, httpOnly, sameSite=lax, path=/
- Session expiry: 30 days, refreshed on each authenticated request
- Sessions stored in SQLite — simple cleanup of expired rows
## Frontend Changes
### Login button (top-right, Gitea-style)
- When not logged in: "Sign in" button in the header/navbar
- When logged in: username display with dropdown (logout, settings link)
### Login page
- Route: `/login`
- Simple form: username + password + submit
- Redirects back to previous page on success
- Error message on invalid credentials
### Initial setup
- If `GET /api/auth/me` returns `null` and no users exist, show a setup prompt
- Setup form: create username + password
- Only shown once — after account creation, normal login flow applies
### Conditional UI
- Add/edit/delete buttons: hidden when not authenticated
- Forms (ItemForm, CandidateForm, etc.): only accessible when authenticated
- The app is fully browseable without login — you just can't modify anything
### Auth state
- `useAuth` hook using React Query: calls `GET /api/auth/me`
- Returns `{ user, isAuthenticated, login, logout }`
- Cached and invalidated on login/logout
### Settings page additions
- API key management section: list, create, revoke
- Change password form
## Testing
- Auth service tests: login, logout, session creation/validation, password change
- API key tests: create, verify, revoke
- Middleware tests: write endpoints reject without auth, read endpoints work without auth
- Setup flow test: first-user creation
## Security Considerations
- Passwords hashed with argon2 via Bun built-in
- Session tokens are cryptographically random
- API keys hashed before storage (only shown once on creation)
- httpOnly cookies prevent XSS access to session
- No CORS changes needed (single-origin app)

View File

@@ -0,0 +1,87 @@
# Image URL Fetching — Design Spec
## Overview
Add the ability to fetch images from external URLs via the API, download them to local storage, and preserve the original source URL as metadata. API-only feature — no frontend changes.
## New Endpoint
### `POST /api/images/from-url`
**Request:**
```json
{ "url": "https://example.com/photo.jpg" }
```
**Validation (Zod):**
- `url` — valid URL string, required
**Server-side behavior:**
1. Fetch the URL with a 10-second timeout
2. Check response `Content-Type` is one of: `image/jpeg`, `image/png`, `image/webp`
3. Check `Content-Length` does not exceed 5MB (match existing upload limit)
4. Stream response body to `uploads/` directory using existing naming: `${Date.now()}-${randomUUID()}.${ext}`
5. If any check fails, return 400 with descriptive error
**Response:**
```json
{ "filename": "1712160000000-abc123.jpg", "sourceUrl": "https://example.com/photo.jpg" }
```
Callers can use `filename` for `imageFilename` and `sourceUrl` for `imageSourceUrl` when creating/updating items or candidates.
**Error responses:**
- `400` — invalid URL, unsupported content type, file too large, fetch failed
- `500` — server error during download/save
## Schema Changes
### `items` table
Add column:
```
imageSourceUrl: text("image_source_url") // nullable
```
### `threadCandidates` table
Add column:
```
imageSourceUrl: text("image_source_url") // nullable
```
### Zod schemas
Add `imageSourceUrl: z.string().url().optional()` to:
- `createItemSchema`
- `updateItemSchema`
- `createCandidateSchema`
- `updateCandidateSchema`
### Types
Types are inferred from Zod schemas and Drizzle tables — no manual updates needed.
## Existing Behavior Unchanged
- `POST /api/images` (file upload) remains as-is
- All existing image display, cleanup, and serving logic unchanged
- `imageFilename` continues to work identically
## Test Helper Updates
Add `image_source_url TEXT` column to the `items` and `thread_candidates` CREATE TABLE statements in `tests/helpers/db.ts`.
## Testing
- Service test: fetch from a valid URL, verify file saved and filename returned
- Route test: POST to `/api/images/from-url` with valid/invalid URLs
- Validation tests: wrong content type, oversized image, invalid URL format, timeout

View File

@@ -0,0 +1,158 @@
# MCP Server — Design Spec
## Overview
Built-in MCP server running inside the GearBox Hono process, exposed via SSE transport at `/mcp`. Provides tools for managing the full gear collection with workflow guidance emphasizing research threads. Authenticates via API key.
## Transport & Configuration
- **Transport:** SSE or Streamable HTTP at `/mcp` (use whichever the MCP SDK supports best at implementation time — the newer spec favors Streamable HTTP)
- **Enabled by default**, disable with `GEARBOX_MCP=false` env var
- **Authentication:** API key passed in MCP client config, sent as `X-API-Key` header
- **SDK:** `@modelcontextprotocol/sdk` TypeScript package
## Integration with Hono
The MCP server mounts as a route on the existing Hono app:
```typescript
// src/server/index.ts
if (process.env.GEARBOX_MCP !== "false") {
app.route("/mcp", mcpRoutes);
}
```
The MCP route handler bridges SSE transport to the MCP server instance, which calls GearBox services directly (not via HTTP) for efficiency.
## Tools
All tools include descriptive names and descriptions that guide Claude toward the research thread workflow.
### Item Tools
| Tool | Description |
|------|-------------|
| `list_items` | List all items in the gear collection. Optionally filter by category. |
| `get_item` | Get details of a specific item by ID. |
| `create_item` | Add a new item to the gear collection. Use this for items you've already decided on. For items you're still researching, use create_thread instead. |
| `update_item` | Update an existing item's details. |
| `delete_item` | Remove an item from the collection. |
### Category Tools
| Tool | Description |
|------|-------------|
| `list_categories` | List all gear categories. |
| `create_category` | Create a new category for organizing gear. |
### Thread Tools (Primary Workflow)
| Tool | Description |
|------|-------------|
| `list_threads` | List all research threads. Threads are the recommended way to evaluate gear purchases — create a thread, add candidates, compare them, then resolve to pick a winner. |
| `get_thread` | Get a thread with all its candidates and comparison data. |
| `create_thread` | Start a new research thread for evaluating a gear purchase. This is the preferred workflow: create a thread describing what you need, add candidate products, compare specs/weight/price, then resolve when you've decided. |
| `resolve_thread` | Resolve a thread by picking the winning candidate. This adds the winner to your collection as a new item and marks the thread as resolved. |
### Candidate Tools
| Tool | Description |
|------|-------------|
| `add_candidate` | Add a candidate product to a research thread. Include weight, price, pros, cons, and optionally an image URL. |
| `update_candidate` | Update a candidate's details — weight, price, pros, cons, etc. |
| `remove_candidate` | Remove a candidate from a research thread. |
### Setup Tools
| Tool | Description |
|------|-------------|
| `list_setups` | List all gear setups (named configurations of items). |
| `get_setup` | Get a setup with all its items, total weight, and total cost. |
| `create_setup` | Create a new gear setup. |
| `update_setup` | Update a setup's items or details. |
### Image Tools
| Tool | Description |
|------|-------------|
| `upload_image_from_url` | Fetch an image from a URL and attach it to an item or candidate. |
## Resources
### `gearbox://collection-summary`
Provides an overview of the current gear collection:
- Total items, total weight, total cost
- Items per category
- Active research threads
- Number of setups
This resource gives Claude context about the collection state before making tool calls.
## Workflow Guidance
The MCP server's tool descriptions are crafted to guide Claude toward the research thread pattern:
1. When the user asks about buying gear, Claude should prefer `create_thread` over `create_item`
2. Candidates are added to threads for comparison before committing
3. `resolve_thread` is the way to finalize a purchase decision
4. Direct `create_item` is for items already owned or decided on
This guidance lives in the tool descriptions themselves — no separate system prompt needed. The `collection-summary` resource helps Claude understand what's already in the collection.
## Implementation Structure
```
src/server/mcp/
index.ts — MCP server setup, tool/resource registration
tools/
items.ts — Item tool handlers
categories.ts — Category tool handlers
threads.ts — Thread + candidate tool handlers
setups.ts — Setup tool handlers
images.ts — Image tool handlers
resources/
collection.ts — Collection summary resource
```
## Client Configuration
### Claude Code (`.claude/settings.json`)
```json
{
"mcpServers": {
"gearbox": {
"type": "sse",
"url": "http://localhost:3000/mcp",
"headers": {
"X-API-Key": "<your-api-key>"
}
}
}
}
```
### Claude Desktop
```json
{
"mcpServers": {
"gearbox": {
"type": "sse",
"url": "http://localhost:3000/mcp",
"headers": {
"X-API-Key": "<your-api-key>"
}
}
}
}
```
## Testing
- Tool handler tests: each tool with valid/invalid inputs
- Auth test: requests without API key are rejected
- Resource test: collection summary returns accurate data
- Integration test: create thread -> add candidates -> resolve -> verify item created