Files
st-pierre/AGENTS.md
T
2026-06-16 11:16:01 -04:00

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 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 <token> header. Token set via MCP_TOKEN env var.