4.3 KiB
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
# 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 KEYfor auto-increment IDsSetMaxOpenConns(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) — embedspublic/viaembed.FSpublic_fs_dev.go(//go:build dev) — serves from disk viahttp.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 <token> header. Token set via MCP_TOKEN env var.