A minimal, DokuWiki-style personal wiki backed by semlix. Your notes are plain Markdown files on disk; semlix provides fast lexical + semantic (hybrid) search over them.
Two front doors:
- a web UI + JSON API (FastAPI, server-rendered, no build step), and
- an MCP server + Claude Code skill, so an LLM (Claude or any MCP client) can read, write, and search your notes as an ultra-fast embedded backend.
Notes are the source of truth (.md files); the semlix index is always
rebuildable from them.
cd wiskill
uv venv --python 3.11 # create .venv
uv pip install -e /path/to/semlix # semlix as an imported library
uv pip install -e ".[dev,semantic,mcp]" # wiskill + test/semantic/mcp extrasExtras: semantic (sentence-transformers for hybrid/semantic search),
mcp (the MCP server), bm25 (the fast bm25s lexical engine), dev (pytest).
export WISKILL_SECRET="$(python -c 'import secrets;print(secrets.token_urlsafe(32))')"
uv run wiskill init # create the wiki + seed page
uv run wiskill user add me --role admin --password 'change-me'
uv run wiskill serve # http://127.0.0.1:8000Open http://127.0.0.1:8000, sign in as me, and start writing. The + New
button asks for a slug (e.g. projects/semlix) and opens the visual Markdown
editor.
Everything is configurable; defaults are fully local and need no database.
[paths]
pages = "data/pages" # .md files (source of truth)
index = "data/index" # semlix index (rebuildable)
[search]
mode = "hybrid" # lexical | semantic | hybrid
lexical_engine = "core" # core | bm25
alpha = 0.5 # semantic weight in hybrid (0 = lexical, 1 = semantic)
fusion = "rrf" # rrf | linear | dbsf
[semantic]
provider = "sentence-transformers" # | openai | cohere
model = "all-MiniLM-L6-v2"
vector_store = "numpy" # numpy | faiss | pgvector
[auth]
users_file = "data/users.json"
apikeys_file = "data/apikeys.json"
session_secret_env = "WISKILL_SECRET"
session_ttl_hours = 168Search modes
lexical— keywords only (core engine, with highlighted snippets). No model.semantic— meaning only (vector search).hybrid(default) — both, fused with RRF. Localsentence-transformers+NumpyVectorStore, no database.
Point --config /path/to/wiskill.toml at any config; relative paths in it
resolve against its own directory. With no --config, wiskill uses
./wiskill.toml if present, otherwise built-in defaults.
- A page is one
.mdfile with YAML front-matter (title,tags,created,updated). The slug is the path without.md. - Namespaces are folders:
projects/semlix,notes/2026-07. - Link between pages with
[[slug]]or[[slug|label]]. A link to a missing page renders dashed/red and takes you to its editor. - Content is GitHub-Flavored Markdown (tables, task lists, strikethrough, fenced code, autolinks).
If you edit files outside the app (your editor, git, or an LLM writing files
directly), run uv run wiskill reindex to sync the index to disk (it reindexes
changed files and drops deleted ones by content hash).
Secrets never go in wiskill.toml — that file is meant to be versioned, and the
toml only stores the name of the secret's env var (session_secret_env), not
its value. Set secrets via the environment, or drop them in a gitignored
.env that wiskill loads automatically (from the config file's directory and
the current directory). A real environment variable always wins over .env.
cp .env.example .env # then fill it in# .env (gitignored — never commit)
WISKILL_SECRET=<python -c "import secrets;print(secrets.token_urlsafe(32))">
WISKILL_API_KEY=<an editor key from `wiskill apikey add`, or leave empty>With .env in place you can just run uv run wiskill serve / uv run wiskill mcp — no manual export each time.
Roles (increasing): reader < editor < admin.
Read/search need reader; create/edit/delete need editor; managing users and
keys needs admin.
Public (anonymous) read. Set public_read = true under [web] in
wiskill.toml to let anyone view and search without logging in — editing,
creating, and deleting still require an account (editor). The UI hides the edit
controls for guests, and pages are marked robots: index, follow only when
public read is on (otherwise noindex). With public_read = false (default),
login is required for everything.
Web — multi-user, session cookie:
uv run wiskill user add alice --role editor --password '...'
uv run wiskill user list
uv run wiskill user passwd alice --password 'new-pass' # change a password
uv run wiskill user role alice --role admin
uv run wiskill user rm alicePasswords are stored as salted scrypt hashes (never plaintext). The session
cookie is signed with the secret from the WISKILL_SECRET environment variable
— set it before serve.
API + MCP — API key in a header:
uv run wiskill apikey add my-key --role editor
# → prints the plaintext key ONCE. Copy it now; only its sha256 hash is stored.
uv run wiskill apikey list
uv run wiskill apikey rm my-keyEvery /api/* route requires an API key, sent as either
Authorization: Bearer <key> or X-API-Key: <key>. Interactive OpenAPI
docs are at /docs.
KEY=<the key from `wiskill apikey add`>
BASE=http://127.0.0.1:8000
# create / overwrite a page (editor)
curl -X PUT "$BASE/api/pages/notes/idea" -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{"title":"Idea","tags":["draft"],"body":"# Hello\n\nSome **notes**."}'
curl "$BASE/api/pages" -H "X-API-Key: $KEY" # list slugs
curl "$BASE/api/pages/notes/idea" -H "X-API-Key: $KEY" # read one page
curl "$BASE/api/search?q=notes" -H "X-API-Key: $KEY" # search
curl -X DELETE "$BASE/api/pages/notes/idea" -H "X-API-Key: $KEY"GET /api/pages takes optional namespace and/or tag query params to list
only a subset of slugs instead of every page — useful when an external
consumer wants to sync just one corner of the wiki:
curl "$BASE/api/pages?namespace=skills" -H "X-API-Key: $KEY" # only under skills/
curl "$BASE/api/pages?tag=skill" -H "X-API-Key: $KEY" # only pages tagged "skill"
curl "$BASE/api/pages?namespace=skills&tag=skill" -H "X-API-Key: $KEY" # both (AND)Missing/invalid key → 401; insufficient role → 403; missing page → 404.
No code or validation enforces this — it's a convention for wiki authors
whose pages get consumed by something that treats them as skills (e.g. an
agent syncing GET /api/pages?namespace=skills&tag=skill elsewhere). Start
the page's body with a top-level heading that's a one-line description of
what the skill does:
# Summarizes a PDF into three bullet pointsA consumer uses that heading's text as the skill's description. Pages without one still export fine — just with a less precise fallback description on the consumer's side.
The MCP server exposes five tools: wiki_search, wiki_read, wiki_write,
wiki_list, wiki_delete.
Run the web UI, JSON API, and MCP from a single process so they share one
backend (index + vector store). This is the correct way when using hybrid
mode — two separate processes would each keep their own in-memory vector store
and clobber each other's writes.
uv run wiskill serve --with-mcp # web at :8000, MCP at :8000/mcp/Register it in Claude Code (note the trailing slash):
claude mcp add --transport http wiskill http://127.0.0.1:8000/mcp/A page written via MCP is instantly visible in the web UI (and vice-versa),
because both go through the same WikiService.
1. Generate a key (optional).
uv run wiskill apikey add claude --role editor # prints the key onceThe MCP server reads its key from the WISKILL_API_KEY environment variable and
maps it to that key's role. If no valid key is provided, it falls back to the
editor role — convenient for a trusted, local LLM on your own machine. Set a
key (e.g. a reader key) only when you want to restrict what the LLM can do.
2. Run it. Pick a transport:
# stdio (default) — the client launches this process and pipes JSON-RPC.
uv run wiskill mcp # trusted-local: editor role
WISKILL_API_KEY=<key> uv run wiskill mcp # or with an explicit key/role
# HTTP (Streamable HTTP) — a long-running network server, connect by URL.
uv run wiskill mcp --transport http --host 127.0.0.1 --port 8765
# → http://127.0.0.1:8765/mcp
# SSE (legacy) — http://host:port/sse
uv run wiskill mcp --transport sse --port 8765Security: MCP does not have raw TCP — "over the network" means HTTP. The HTTP/SSE transports are not authenticated by wiskill (they use the same trusted-local editor fallback), so bind to
127.0.0.1(the default). Do not expose them on0.0.0.0or a public interface without putting an authenticating reverse proxy in front — anyone who can reach the port gets write access to your notes.
3a. Connect via stdio — add an .mcp.json at your project root (or run
claude mcp add). Use absolute paths so it works from any directory:
{
"mcpServers": {
"wiskill": {
"command": "/absolute/path/to/wiskill/.venv/bin/wiskill",
"args": ["--config", "/absolute/path/to/wiskill/wiskill.toml", "mcp"],
"env": { "WISKILL_API_KEY": "<your editor key, or omit for trusted-local>" }
}
}
}3b. Connect via HTTP — start the server (--transport http above), then
register the URL:
claude mcp add --transport http wiskill http://127.0.0.1:8765/mcp
# SSE instead: claude mcp add --transport sse wiskill http://127.0.0.1:8765/sseThen restart Claude Code (or run /mcp) to pick up the server. The
skill/SKILL.md file documents the tools and conventions for the agent.
For stdio you can omit the
envblock and instead putWISKILL_API_KEYin the gitignored.envnext towiskill.toml—wiskill mcploads it automatically.
The MCP server speaks JSON-RPC on stdout; model-loading chatter and library warnings are redirected to stderr so they can't corrupt the protocol.
wiskill [--config PATH] <command>
init create pages dir + a seed page, then reindex
reindex sync the index to the .md files on disk
serve [--host --port] run the web UI + JSON API (uvicorn)
mcp run the stdio MCP server
user add NAME --role --password
user passwd NAME --password
user role NAME --role
user list
user rm NAME
apikey add LABEL --role (prints the key once)
apikey list
apikey rm LABEL
uv run python -m pytest -q # full suite (uses a fake embedder; no downloads)Architecture: a pure core (PageStore over files + SearchBackend over semlix,
orchestrated by WikiService) with three thin adapters (FastAPI web+API, MCP
server, CLI). Every write goes through WikiService, so the index and disk never
diverge. See docs/superpowers/specs/ for the design and plan.
BSD-2-Clause.