# St-Pierre — Flashcard MCP Server MCP server that stores and retrieves flashcards using SQLite for persistence, Qdrant for vector search, and OpenAI-compatible embeddings (LiteLLM proxy). ## Tech Stack - Go (1.26), flat package `main` - MCP server: `github.com/mark3labs/mcp-go` - SQLite: `modernc.org/sqlite` (pure Go, no CGo) - Migrations: `github.com/pressly/goose/v3` (embedded via `//go:embed migrations/*.sql`) - Vector DB: Qdrant via `github.com/qdrant/go-client` - Embeddings: OpenAI-compatible API (LiteLLM at `https://llm.aviron.ai`) - Logging: `go.uber.org/zap` - Deployed on **enlil** (NixOS server) at `https://st-pierre.home.all-dressed-programming.com/mcp` ## Repository Structure ``` *.go Flat package main (no cmd/ layout) main.go Entry point, MCP server wiring, auth middleware api.go Tool definitions (store_flashcard, search_flashcard) + vote sqlite.go SQLite store — CRUD, search, vote, migration runner fts5.go FTS5 full-text search queries qdrant.go Qdrant gRPC client — upsert, search, collection management embed.go OpenAI-compatible embeddings client search.go Search engine that combines SQLite + Qdrant results types.go Go types (Flashcard, searchRequest, etc.) migrations/ Goose SQL migrations (embedded) public/ Frontend (served via embed.FS or disk in dev mode) public_fs.go Production: embed public/ (no build tag) public_fs_dev.go Dev mode: serve public/ from disk (build tag: dev) flake.nix Nix build + NixOS module ``` ## Build & Run ```bash # Build nix build # Dev (hot-reload frontend) go build -tags dev -o st-pierre . ./st-pierre # Run directly export OPENAI_API_KEY=sk-... MCP_TOKEN=my-token ./st-pierre ``` ## Conventions ### Goose Migrations **NEVER modify an applied migration file.** Goose v3 tracks checksums in `goose_db_version`. Modifying an applied migration causes "checksum mismatch". Always add a new migration with a higher version number. If you must recover a corrupted DB: `DELETE FROM goose_db_version WHERE version_id = N;` or `DROP TABLE goose_db_version;` to reset entirely. ### SQLite - `modernc.org/sqlite` — pure Go, no CGo required - Migrations embedded via `//go:embed migrations/*.sql` - `INTEGER PRIMARY KEY` for auto-increment IDs - `SetMaxOpenConns(1)` mandatory (single-writer SQLite) - Append-only design — never UPDATE rows - Default timestamps: `datetime('now')` - Nullable columns use `sql.NullString` - JSON arrays stored as text columns ### Nix After any `go.mod` or `go.sum` change, update `vendorHash` in `flake.nix` with the hash from a failed build attempt. Never commit go.mod/go.sum changes without updating `vendorHash`. `lib.mkIf` is incompatible with the `//` merge operator — it can silently drop attributes. Use `lib.optionalAttrs` instead when merging with `//`. ## Architecture Notes ### MCP path The MCP server is served at `/mcp` via `http.ServeMux`, not at root. CORS wraps the entire mux. The `result/` symlink created by nix builds is in `.gitignore`. ### Embedding URL normalization `EMBEDDING_BASE_URL` strips any trailing `/v1` and always appends `/v1/embeddings`. A bare host like `https://llm.aviron.ai` correctly becomes `https://llm.aviron.ai/v1/embeddings`. The default is `https://api.openai.com`. ### Dev build tag Two files control how `public/` is served: - `public_fs.go` (`//go:build !dev`) — embeds `public/` via `embed.FS` - `public_fs_dev.go` (`//go:build dev`) — serves from disk via `http.Dir("public")` Build with `go build -tags dev` for frontend hot-reload during development. ## Troubleshooting ### SQLite readonly on NixOS `SQLITE_READONLY_CANTINIT (1032)` means the database file is owned by root due to `createHome` + `StateDirectory` interaction. Fix: add `chown` in the systemd unit `preStart`. Also verify the directory is writable with a `SELECT 1` ping in `newSqliteStore`. ### Embedding API 404 Empty 404 from embedding API means the URL path is wrong. Verify `EMBEDDING_BASE_URL` is set correctly — the server normalizes the base URL and appends `/v1/embeddings`. For LiteLLM proxy, leave it bare: `EMBEDDING_BASE_URL=https://llm.aviron.ai`. ### MCP endpoint The full endpoint is `https://st-pierre.home.all-dressed-programming.com/mcp`. Auth via `Authorization: Bearer ` header. Token set via `MCP_TOKEN` env var.