Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting Started

This chapter covers installing Dr Strange — from a released binary or from source — initializing a database, issuing queries from the command line, and running the server, both locally and as a container image.

Prerequisites

Installing a released binary requires nothing beyond curl (or PowerShell on Windows). The remaining prerequisites apply only to the other routes:

  • Building from source: a current Rust toolchain (stable channel), installed via rustup.
  • The web dashboard: bun to compile the single-page application, and optionally just as the task runner.
  • The container workflow: Docker (Engine 24+; BuildKit is only needed if you build the image yourself).

The build links TLS through rustls/ring; no OpenSSL toolchain is required.

Installing a released binary

Every tagged release publishes binaries for Linux, macOS, and Windows. The installer selects the archive matching the host platform, verifies its published SHA-256, and places the binary on the PATH. Two binaries are available: the command-line tool and server, drsg, and the MCP server for LLM agents, drsg-mcp (Chapter 8).

Linux

# CLI and server — drsg
$ curl -fsSL https://raw.githubusercontent.com/wangyingsm/dr-strange/master/scripts/install.sh | sh

# MCP server — drsg-mcp
$ curl -fsSL https://raw.githubusercontent.com/wangyingsm/dr-strange/master/scripts/install.sh | sh -s -- --bin drsg-mcp

macOS — the same script; both Apple silicon and Intel are published.

# CLI and server — drsg
$ curl -fsSL https://raw.githubusercontent.com/wangyingsm/dr-strange/master/scripts/install.sh | sh

# MCP server — drsg-mcp
$ curl -fsSL https://raw.githubusercontent.com/wangyingsm/dr-strange/master/scripts/install.sh | sh -s -- --bin drsg-mcp

Windows, in PowerShell. The second form runs the script as a block because a script piped into iex cannot receive arguments.

# CLI and server — drsg
PS> irm https://raw.githubusercontent.com/wangyingsm/dr-strange/master/scripts/install.ps1 | iex

# MCP server — drsg-mcp
PS> & ([scriptblock]::Create((irm https://raw.githubusercontent.com/wangyingsm/dr-strange/master/scripts/install.ps1))) -Bin drsg-mcp

Three options adjust the installation, each with an environment-variable equivalent for non-interactive use:

Option (Windows)Environment variableEffect
--bin drsg-mcp (-Bin)DRSG_INSTALL_BINwhich binary — drsg (default), drsg-mcp, or all
--version v1.1.0 (-Version)DRSG_VERSIONpin a release instead of the latest
--dir <path> (-Dir)DRSG_INSTALL_DIRdestination directory

The destination defaults to ~/.local/bin, and to %LOCALAPPDATA%\Programs\drsg\bin on Windows, where the installer also adds the directory to the user PATH. On Linux and macOS, add it to the shell profile if it is not already present:

$ export PATH="$HOME/.local/bin:$PATH"

Upgrading

drsg update resolves the newest release the way the installer does — through the redirect on releases/latest, rather than the API, which is rate-limited for unauthenticated callers — and compares it with the running build. When there is nothing to do it says so and stops:

$ drsg update
drsg 2.27.1 is the latest release — nothing to do

When there is, it prints the command it is about to run and then becomes it: the process is replaced by the same installer a first install runs, so the exit status is the installer’s own and there is no parent left waiting in a binary that has just been overwritten.

$ drsg update
drsg 2.27.0 -> 2.27.1
$ curl -fsSL https://raw.githubusercontent.com/wangyingsm/dr-strange/v2.27.1/scripts/install.sh | sh -s -- --bin 'drsg' --version 'v2.27.1' --dir '/home/me/.local/bin'
Dr Strange v2.27.1 (x86_64-unknown-linux-gnu)
  downloading dr-strange-v2.27.1-x86_64-unknown-linux-gnu.tar.gz
  checksum verified
  installed /home/me/.local/bin/drsg

Both halves cross the network, and both honour a proxy: the version check is a drsg request (see Working behind an HTTP proxy), and the installer it hands over to is curl, which reads the same variables. On a network that needs a proxy the version check is what fails first, so an update that cannot reach GitHub never reaches the installer at all.

The installer it runs is the one tagged with the release being installed, not the copy on master, and the release is passed along as --version: the script that verifies the archive is the script reviewed and shipped with it, and moving a branch cannot change what an upgrade executes. The archive’s .sha256 sidecar is required by both installers — a missing, malformed or mismatching checksum is a hard failure, as is having no sha256sum/shasum to check with. --insecure-skip-checksum (-InsecureSkipChecksum in PowerShell; DRSG_INSECURE_SKIP_CHECKSUM=1 for either, and for drsg update) installs anyway, with a warning: it exists for a mirror that publishes no sidecar, and its name says what it costs.

The destination is the directory the running binary is in, not the installer’s default — an upgrade must replace the copy on the PATH, not add a newer one somewhere else and leave the old one being run. --dir overrides it for a drsg installed somewhere unwritable. A drsg-mcp in that same directory is updated alongside drsg without being asked — the two binaries are one release, and an agent host launching last release’s server against this release’s drsg would have nothing to tell it so. --bin names exactly what to update when that is not wanted: drsg, drsg-mcp, or all.

A build newer than the latest release — from source, or from a branch ahead of the last tag — is told it is ahead and nothing is installed: update never moves backwards. On Windows nothing is run at all, because the running executable is locked against being overwritten; the command to paste into a fresh terminal is printed instead.

The archives and their checksums may also be downloaded directly from the releases page; the installers are a convenience over the same assets, and both scripts live in scripts/ for inspection before use.

Building from source

Dr Strange is a Cargo workspace. Compile the command-line binary, drsg:

$ cargo build --release -p drsg

The artifact is target/release/drsg. The default build selects the native LSM storage engine; the legacy redb backend remains available behind a feature flag.

The web dashboard is embedded into the binary at compile time by the web crate’s build script. To embed the compiled dashboard rather than a placeholder page, build the single-page application before the binary:

$ just web-build          # bun install && vite build
$ cargo build --release -p drsg

The MCP server for LLM agents (Chapter 8) is a separate binary, drsg-mcp:

$ cargo build --release -p dr-strange-mcp

The artifact is target/release/drsg-mcp. Place it on the PATH, or reference it by absolute path in the host configuration.

On-disk layout

The --db argument selects the database path. Under the native backend the database is a directory — the write-ahead log and the sorted SST files reside within it — accompanied by two sidecar files that hold the search indexes:

graph.drsg/          database (WAL + SST files)
graph.drsg.hnsw      vector-index sidecar
graph.drsg.bm25      keyword-index sidecar

The database is created on first access; there is no separate initialization step.

Creating a graph

Create a plane, then insert data. The openCypher subset compiles to the same logical plan the engine executes directly:

$ drsg --db graph.drsg plane create social

$ drsg --db graph.drsg cypher --plane social \
    'CREATE (a:Person {name:"Ada"}),
            (b:Person {name:"Alan"}),
            (a)-[:KNOWS]->(b)'

Query the result:

$ drsg --db graph.drsg cypher --plane social \
    'MATCH (p:Person)-[:KNOWS]->(q:Person) RETURN q'

Inspect the resulting shape and soft schema:

$ drsg --db graph.drsg stats
$ drsg --db graph.drsg catalog --plane social

Vectors are ordinary property values. Declare an index on a (label, property) pair, then query it. Embedding a text query is performed server-side and therefore requires a provider key in the process environment (for example, OPENAI_API_KEY); a query against a literal vector requires no provider.

$ drsg --db graph.drsg index ensure Doc embedding --plane social

$ OPENAI_API_KEY=… drsg --db graph.drsg cypher --plane social \
    'SEARCH (d:Doc) ON embedding NEAR "a friendly greeting" TOPK 5 RETURN d'

Because the results are graph nodes, traversal can continue from them — the GraphRAG pattern introduced in Chapter 1.

Running the server

$ drsg --db graph.drsg serve

This starts the JSON-RPC 2.0 API, the WebSocket change feed, and the embedded dashboard, and reports the bound address (default 127.0.0.1:7700).

Authentication. With no token configured, only the same-origin browser UI is authorized to call the API. To permit programmatic access from the SDKs or curl, configure a shared token and present it as a bearer credential:

$ DRSG_TOKEN=please-change-me drsg --db graph.drsg serve

A token is required for any bind address other than loopback: drsg serve --addr 0.0.0.0:7700 without DRSG_TOKEN (or [server] token) refuses to start, because off loopback the same-origin check is no protection at all. On such an address the served dashboard does not carry the token either — the browser asks for it once per tab (see the Web UI chapter) — and the dashboard’s own origin must be named in allowed_origins. Send the token as Authorization: Bearer <token> on HTTP and, preferably, on the WebSocket upgrade as well; ?token= on /ws exists for browsers, which cannot set headers there, and a query string is what proxies and browsers tend to log. A peer that presents a wrong token more than five times is answered 429 with a Retry-After that doubles up to five minutes; a correct token clears it.

Over the API a provider name is one of the presets or exactly the server’s configured embed_provider; a base URL is accepted from the CLI and the configuration file, never from a request.

Configuration file

Server, logging, and provider settings may be supplied through a TOML configuration file instead of individual flags and environment variables. The file is resolved from --config <path>, then $DRSG_CONFIG, then ./drsg.toml if present. Unknown keys are rejected.

[server]
addr = "0.0.0.0:7700"                       # bind address (CLI --addr overrides)
token = "please-change-me"                  # shared API token (→ DRSG_TOKEN); prefer exporting it, see below
max_concurrent = 1024                       # ceiling on in-flight requests
write_timeout_secs = 30                     # how long a write waits for the single writer slot before failing retryably (0 waits forever)
query_timeout_secs = 60                     # how long one request's queries may run before stopping the same way (0 runs to completion)
retain_commits = 20                         # commits of history time-travel can reach; older versions are reclaimed (0 keeps all). Applies to every drsg command, not only serve
source_root = "/srv/myrepo"                 # source tree behind the grep/snippet agent tools (serve watch sets it from --dir)
mount_root = "/srv/repos"                   # the one directory `drsg mount` may open a database from (default: your home directory); a plane inside it is also readable by grep/snippet
allowed_origins = ["https://app.example.com"]  # additional browser origins
# page_token = false                        # never write the token into the page (→ DRSG_PAGE_TOKEN); for a loopback bind behind a reverse proxy, see the Web UI chapter
# /mcp behind a hostname: DRSG_ALLOWED_HOSTS=memory.example.com (env; honoured only with a token — see the MCP chapter)

[server.tls]                                # present ⇒ serve HTTPS
cert = "/etc/drsg/cert.pem"                 # PEM certificate chain
key  = "/etc/drsg/key.pem"                  # PEM private key

[logging]
dir = "/var/log/drsg"                       # directory for the rolling log file

[llm]                                       # provider keys, exported to the environment
OPENAI_API_KEY = "sk-…"
DEEPSEEK_API_KEY = "…"
DASHSCOPE_API_KEY = "…"
DRSG_CHAT_OMIT = "temperature,max_tokens"   # optional: chat fields to leave out, for a model that rejects them

[digest]                                    # server-side AIgest tuning
concurrency = 8                             # per-chunk extraction calls in flight
chunk_chars = 4000                          # target chunk size
embed_provider = "openai"                   # embedding provider for search / write_nodes / watch re-vectorization
embed_model = "text-embedding-3-small"      # its model (each provider has a default)
embed_key_env = "OPENAI_API_KEY"            # env var holding its key
ignore_files = ["gitignore"]                # whose ignore files decide what is source; [] honours neither
tracked_only = false                        # true reads only what git tracks
code_only = false                           # true reads only what a handler claims (no Markdown/PDF)

[plugins]                                   # preprocessor sandbox tuning (all optional)
fuel = 200000000000                         # instruction budget per sandbox call (0 disables)
memory_mb = 3072                            # guest linear memory per call, MiB (wasm32 allows at most 4096)
deadline_secs = 300                         # wall-clock ceiling per call, seconds (0 disables)
total_memory_mb = 6144                      # linear memory all calls may hold together, MiB (0 = twice memory_mb)
hostcall_mb = 1024                          # what one call may copy out of the sandbox, MiB (0 = wasmtime's 128)

[fetch]                                     # URL ingestion (Chapter 3)
enabled = true                              # false refuses URL fetching outright
max_pages = 10                              # ceiling on pages kept per crawl
max_depth = 3                               # ceiling on link-following depth a request may ask for
concurrency = 4                             # requests in flight
allow_private = []                          # see below — normally left empty

[network]                                   # where this binary's own requests go
proxy = "http://127.0.0.1:7897"             # or socks5://…; overridden by ALL_PROXY / HTTPS_PROXY / HTTP_PROXY
no_proxy = ".internal, 192.168.1.10"        # NO_PROXY-style bypass list; overridden by NO_PROXY

[fetch] changes the server’s network posture, and is worth reading before enabling anything in it. With URL ingestion on — the default — a client can name an address that the server then connects to. The server’s position on the network is usually the more privileged one, so every non-routable address is refused: loopback, RFC-1918 private space, link-local (169.254.0.0/16, where cloud instance metadata services answer credentials), and the rest. The check is made on the resolved address, not the hostname, and repeated at every redirect hop; an IPv6 address that carries an IPv4 one inside it (v4-mapped, 6to4, Teredo, NAT64’s 64:ff9b::/96) is judged as that IPv4 address.

allow_private re-permits specific CIDR blocks — ["10.0.0.0/8"] to read an intranet wiki — and is the one deliberate exception to that. It is not a switch that turns the guard off, and a server reachable by untrusted clients should leave it empty. To refuse URL fetching entirely, set enabled = false.

Working behind an HTTP proxy

[network] governs the requests drsg makes on your behalf: drsg plugin install, drsg update, drsg digest <url>, the LLM provider, and a replica’s snapshot fetch. Set it in the file, or export any of ALL_PROXY, HTTPS_PROXY or HTTP_PROXY (each also read lowercase). socks5:// works as well as http://.

Three rules are worth knowing, because each one is a decision rather than an accident:

A URL a caller names is never proxied. /digest/fetch and plugin.install over JSON-RPC take a URL from whoever is calling, and the address guard described above is what stops that URL naming the server’s own loopback or a cloud metadata endpoint. A proxy resolves the name and connects on the server’s behalf, so the guard would have no address left to judge — it cannot be enforced through a proxy, and drsg does not pretend otherwise. Those requests stay direct and guarded. drsg serve says so once at startup when a proxy is configured.

Loopback is never proxied, whatever is configured. A local model server — the ollama preset is http://localhost:11434/v1 — is not what anyone setting https_proxy for the outside world meant to redirect. Any other address obeys the configuration; only localhost and the loopback ranges are automatic.

The environment wins. A variable that is set overrides the file, so a single command can go through a different proxy without editing anything:

https_proxy=http://127.0.0.1:1080 drsg plugin install ts   # this run only
https_proxy= drsg plugin install ts                        # set but empty: go direct

no_proxy takes a NO_PROXY-style list, comma- or space-separated: an exact host, a .suffix or *.suffix wildcard, an IP literal, an entry with a port (example.com:8080), or * for everything. CIDR blocks are not supported. NO_PROXY in the environment replaces the file’s list rather than adding to it.

Precedence is fixed: an environment variable already set in the process always takes precedence over the corresponding file value, and the --addr flag overrides [server].addr. Providing [server.tls] switches the server to HTTPS.

Secrets in the file become process environment. token and every [llm] key are applied with setenv at startup, so they are inherited by every process drsg starts — the detached serve watch that drsg init spawns, plugin hosts, anything a hook runs — and are readable in /proc by the same user. That is what makes the provider layer’s “look the key up by env-var name” work, and it is why the shipped drsg.example.toml leaves token commented out: a copied example with a well-known token is a server with no token. Keep a drsg.toml holding secrets out of version control, or export them in the launching environment and leave them out of the file.

retain_commits is read by every drsg command that opens the database, not only serve: a store driven from the command line — import, cypher, digest, vectorize — lives by the same window (and the same default of 20) as it would under the server, so its history is reclaimed at compaction rather than kept forever. The stdio drsg-mcp binary reads no config file but opens with the same default; DRSG_RETAIN_COMMITS changes it there (Chapter 8).

Read-only replicas (serve --follow)

A second drsg serve, started with --follow, mirrors a running one read-only — for scaling reads across a cluster of agents without funnelling every query through one process:

$ drsg --db replica.drsg serve --addr 127.0.0.1:7701 \
    --follow ws://master-host:7700 --follow-token please-change-me

Every write RPC is refused regardless of token. On startup — and again after any disconnect — the replica pulls a full, consistent snapshot from the master’s /snapshot endpoint, then tails its /ws/wal for new commits; every reconnect resyncs from scratch (arch/01 §9), so --db must name an empty directory or one this same replica already owns (a .drsg-follower marker records that) — anything else is refused rather than silently wiped.

--follow-token (or DRSG_FOLLOW_TOKEN) is the credential presented to the master; it is independent of this replica’s own DRSG_TOKEN, which still gates its own downstream clients. Point --follow at wss:// rather than ws:// for a master reachable over anything but loopback — the bearer token otherwise travels in plaintext, the same requirement [server.tls] already carries for inbound connections.

Container image

A multi-arch image (linux/amd64 and linux/arm64) is published to the GitHub Container Registry. Pull and run it — no build required:

$ docker run -p 7700:7700 -v drsg-data:/data \
    -e DRSG_TOKEN=please-change-me \
    ghcr.io/wangyingsm/dr-strange:latest

docker run pulls the image on first use. Pin a release with a version tag — ghcr.io/wangyingsm/dr-strange:1.0.2 — instead of :latest for reproducible deployments. The runtime image binds to 0.0.0.0:7700 and stores the database on the /data volume (the native backend database is a directory, which the volume persists). Provider keys are supplied as environment variables.

DRSG_TOKEN is required in a container: the image binds 0.0.0.0, and drsg serve refuses a non-loopback bind without a token rather than serve the API and the dashboard to whoever reaches the port (the container exits with the message naming the variable). Every client presents that token — the SDKs and curl as a bearer, the dashboard through its prompt. Opening the dashboard at http://localhost:7700 on the Docker host works as is; a browser reaching the container by any other name — http://graph.example:7700 — must have that origin listed in DRSG_ALLOWED_ORIGINS (comma-separated, or [server] allowed_origins), because off loopback the served page carries no token and an unlisted origin is refused as cross-site.

For a persistent deployment, docker-compose.yml pulls the same image and defines a named volume. It marks DRSG_TOKEN as required, so an unset token stops docker compose up with a message instead of starting a container that exits; DRSG_ALLOWED_ORIGINS is passed through when set:

$ DRSG_TOKEN=please-change-me docker compose up
$ DRSG_TOKEN=please-change-me DRSG_ALLOWED_ORIGINS=http://graph.example:7700 docker compose up

To build the image locally instead, the repository ships a multi-stage Dockerfile that compiles the dashboard, embeds it in the binary, and produces a minimal runtime image: docker build -t dr-strange:latest ..

Next steps

  • Chapter 3 — AI Native: embeddings, hybrid retrieval, natural-language querying, and document ingestion.
  • Chapter 4 — Query Language: the openCypher subset and the underlying logical plan.
  • By interface: Chapter 6 — SDK (application code), Chapter 7 — Embedded CLI (operations), Chapter 8 — MCP (LLM agents).