Skip to main content
Glama

lore

Living documentation for your services. Every service pushes per-endpoint Markdown docs from CI on each merge; Lore keeps every version, diffs them, and answers questions across all services — in a browser chat that cites its sources, and over MCP for AI agents.

The problem it solves. Hand-written docs drift from the code, and the knowledge you need ("what happens after an order is placed?") is spread across several services. With Lore the docs are rebuilt from the code on every merge (by your own generator, or the example one in this repo), so they describe what is actually deployed, and one question can be answered from the docs of every service involved — with a link to each document it used.

Who it is for. Teams with several services who want searchable, versioned, AI-readable reference docs inside their own network. It is a single Node.js process plus Postgres (pgvector). It is not a docs website generator or a wiki: there is no editor. Every reader signs in (see Security).

How it works

flowchart LR
  subgraph CI["CI of each service (on merge to main)"]
    G[doc generator<br/>OpenAPI / code + LLM] --> D[docs/*.md]
    D --> P["lore push -l service -v $GIT_SHA"]
  end
  P -->|HTTP API, project key lpk_| L[Lore]
  L --- DB[(Postgres + pgvector<br/>versions, links, embeddings)]
  B[Browser chat] -->|question| L
  L -->|answer with ref chips| B
  A[Claude Code / agents] -->|MCP /mcp, personal sk_ token| L
  L -.->|optional| LLM[OpenAI-compatible LLM]
  • Push. lore push registers a version (usually the git SHA), clones the current version, uploads only documents whose content hash changed, deletes documents removed locally, syncs images and promotes the version. Feature branches can push with --no-promote and be queried by branch.

  • Store. Every version is kept. get_document_diff / GET /api/libraries/:id/diff/:slug show what changed between releases. Links between documents come from frontmatter, from metadata extracted from the text (tables, queues, services), and optionally from LLM enrichment.

  • Search. Hybrid: Postgres full-text search plus vector search, fused with reciprocal rank fusion. An LLM (if configured) first turns the question into English search terms, so questions can be asked in any language.

  • Chat. The top results are fetched in full and given to the LLM together with documents linked from them. Answers carry [ref:library/slug] markers that the UI renders as chips; clicking one opens the source document. A brainstorm mode and a "pinned documents" mode are available, and a chat can be published as an insight document into a dedicated insights library, linked to the documents it cited.

  • MCP. Eleven tools (ask_lore, search_docs, get_document, find_related_docs, get_document_diff, ...) over Streamable HTTP, SSE or stdio.

Related MCP server: Agentdocs

Quick start (Docker Compose)

Requirements: Docker with Compose. An OpenAI API key (or any OpenAI-compatible endpoint) is optional; without it search, MCP and pushing work, and the chat shows search results instead of written answers.

git clone https://github.com/ai-pipes/lore.git && cd lore
cp .env.example .env
sed -i.bak "s|^LORE_API_KEY=.*|LORE_API_KEY=$(openssl rand -hex 32)|" .env   # break-glass admin key, required
# the first admin user (password at least 12 characters)
printf 'LORE_ADMIN_LOGIN=admin\nLORE_ADMIN_PASSWORD=%s\n' "$(openssl rand -hex 16)" >> .env
# optional, for written chat answers: add OPENAI_API_KEY=sk-... to .env
docker compose up -d --build
curl http://localhost:4737/health/ready        # {"status":"ok"}

http://localhost:4737 redirects to the sign-in page (/login); sign in with LORE_ADMIN_LOGIN and the password from .env (grep ^LORE_ADMIN_PASSWORD= .env). From there an admin adds users (Users), namespaces and project keys (Namespaces & keys), and everyone creates personal tokens for MCP and scripts (Tokens). To sign in through your identity provider instead of passwords, see Single sign-on.

API clients send Authorization: Bearer <token>: a user's personal sk_ token, a project key lpk_ (pushes only), or the break-glass admin key LORE_API_KEY.

Compose refuses to start without LORE_API_KEY, and the server (which runs with NODE_ENV=production in the image) rejects well-known defaults such as changeme and keys shorter than 24 characters. Load the key into your shell for the commands below: export $(grep ^LORE_API_KEY= .env).

Compose builds the image from source; to run a released image instead, put image: ghcr.io/ai-pipes/lore:1 in place of build: . (see Released images). Compose runs Lore and pgvector/pgvector:pg17. Migrations run on startup. Ports are published on 127.0.0.1 only: 4737 (HTTP API + chat UI), 4738 (MCP), 5433 (Postgres). Embeddings default to the offline hash provider; set LORE_EMBEDDING_PROVIDER=openai together with OPENAI_API_KEY for semantic search (re-push your docs after switching providers).

Demo

examples/demo-docs contains two fictional services in Lore's format: petstore-api (12 documents: pets, adoption orders, refunds, an expiry job, an event, a table) and notifications-service (4 documents, consuming the order event). They include business rules and cross-service links, so the chat has something to answer.

With Node.js 22 on the host:

npm ci
npm run demo          # pushes to LORE_URL (default http://localhost:4737) with LORE_TOKEN, else LORE_API_KEY from .env

or without Node.js on the host:

for lib in petstore-api notifications-service; do
  docker compose exec -T lore sh -c "LORE_TOKEN=\$LORE_API_KEY node dist/cli.js push examples/demo-docs/$lib -l $lib -v demo-1"
done

Then open http://localhost:4737 and ask, for example, "How do refunds work for delivered orders?" or "What happens after an order is placed, across services?". Or search from the shell:

curl -s -H "Authorization: Bearer $LORE_API_KEY" "http://localhost:4737/api/search?q=refund+delivered+order" | jq '.groups[].results[].slug'

npm run demo always talks to LORE_URL (default http://localhost:4737), never to the server stored by lore login, so the local LORE_API_KEY is not sent elsewhere.

Users and roles

Every reader signs in. A user has a login, a display name, a role and a sign-in method:

  • member — reads all docs, searches, chats (with a private chat history), publishes insights and manages their own personal tokens.

  • admin — everything a member does, plus pushing docs to any namespace, the Users page (/admin/users: add users, reset passwords, unlock a locked password sign-in, change roles, disable and enable) and the Namespaces & keys page (/admin/namespaces).

With passwords (the default, no OIDC settings), the first admin comes from LORE_ADMIN_LOGIN / LORE_ADMIN_PASSWORD when the database has no users yet; admins add everyone else. Passwords are 12 to 1024 characters and stored as scrypt hashes; ten failed sign-ins from one IP address within 15 minutes lock that address out for 15 minutes. Disabling a user ends their web sessions at once and makes their sk_ tokens stop working (on /api and MCP); enabling them again restores the tokens. Nobody can disable themselves.

Personal tokens (sk_…, the Tokens page /tokens) authenticate MCP clients, the CLI and scripts as their owner. A token is shown once, together with a ready claude mcp add … command, never expires and can be revoked at any time (tokens minted by lore login expire, see below). Tokens are created, listed and revoked only from a signed-in browser session, so a leaked token cannot mint more.

Chat history is per user: everyone sees only their own sessions, and another user's session id answers 404.

Single sign-on (OIDC)

SSO is optional. When OIDC_ISSUER, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET are all set, the web UI signs in only through the identity provider (Authorization Code + PKCE S256, with state and nonce). Setting some but not all three, or SSO without PUBLIC_URL, is a startup error.

What changes in SSO mode:

  • The sign-in page shows one "Sign in with SSO" button; POST /api/auth/login answers 403 sso_only. Existing sk_ tokens (also of users created with a password before SSO) keep working.

  • Access and role come from the person's groups: a member of OIDC_ADMIN_GROUP is an admin, a member of OIDC_LOGIN_GROUP a member, anyone else is refused. The role is re-synced on every sign-in. A known person who left both groups is refused and loses their web sessions; their tokens are not revoked, so disable the user on the Users page to off-board them completely. SSO web sessions also end on their own: 24 hours after sign-in at the latest (LORE_SSO_SESSION_MAX_HOURS), however active the person is, so the next request sends them to the provider again and their groups and role are re-read.

  • The first sign-in of an unknown person creates a user. Its login is preferred_username, else the local part of a verified email, else sub, normalized, with -2, -3, … on collision.

  • People are managed in the provider: adding users and resetting passwords answer 409 sso_managed. Disabling and enabling still work and win over group membership.

  • LORE_ADMIN_LOGIN / LORE_ADMIN_PASSWORD are ignored (with a warning); the first admin is whoever signs in as a member of OIDC_ADMIN_GROUP. Starting a sign-in is limited to 30 per IP address per 10 minutes.

Provider setup

  1. Create two groups: lore-users (may sign in) and lore-admins (may sign in, and are admins).

  2. Create a confidential OIDC client (Authorization Code with a client secret, PKCE allowed) with the redirect URI <PUBLIC_URL>/api/auth/sso/callback, and make the provider put the groups into the ID token or the UserInfo response (claim groups by default).

  3. Set PUBLIC_URL, OIDC_ISSUER, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET in .env (next to docker-compose.yml, which forwards them and the optional OIDC_* variables below to the lore service) and recreate the container (docker compose up -d). The startup log says Sign-in: single sign-on via <issuer> or Sign-in: password (a partial setting is a configuration error); check with docker compose exec lore env | grep -E '^(PUBLIC_URL|OIDC_)' that the values arrived.

Example: Pocket ID (the reference provider) at https://id.example.com, Lore at https://lore.example.com:

  1. In Pocket ID, User Groups → create lore-users and lore-admins and add people to them.

  2. OIDC Clients → add a client "Lore" with the callback URL https://lore.example.com/api/auth/sso/callback; keep it confidential (not a public client) and copy the client ID and secret. Pocket ID puts the groups claim into the token when the groups scope is requested, which Lore does by default.

  3. .env:

    PUBLIC_URL=https://lore.example.com
    OIDC_ISSUER=https://id.example.com
    OIDC_CLIENT_ID=<client id>
    OIDC_CLIENT_SECRET=<client secret>
    # Lore is published on 127.0.0.1:4737; a TLS-terminating reverse proxy serves
    # https://lore.example.com, forwards the Host header and needs the Secure cookie flag:
    LORE_COOKIE_SECURE=true
  4. docker compose up -d, open https://lore.example.com, and sign in with SSO as a member of lore-admins: that person becomes the first admin.

Other providers: Authentik, Authelia and Kanidm work with the defaults; Keycloak needs a client scope groups with a Group Membership mapper (claim name groups, "Full group path" off); for providers that name the claim differently set OIDC_GROUPS_CLAIM (and OIDC_SCOPES).

Variable

Default

Meaning

PUBLIC_URL

unset (required with SSO)

The URL people open Lore at; the redirect URI is ${PUBLIC_URL}/api/auth/sso/callback. Also the base of the CLI's approval link.

OIDC_ISSUER

unset

Issuer URL; discovery at <OIDC_ISSUER>/.well-known/openid-configuration.

OIDC_CLIENT_ID

unset

Client ID of the confidential client.

OIDC_CLIENT_SECRET

unset

Client secret.

OIDC_LOGIN_GROUP

lore-users

Members may sign in (as members).

OIDC_ADMIN_GROUP

lore-admins

Members may sign in and are admins.

OIDC_SCOPES

openid profile email groups

Requested scopes (openid is required).

OIDC_GROUPS_CLAIM

groups

Claim with the groups: a list of names, or an object whose keys are the names.

OIDC_ALLOW_INSECURE

false

Allow a plain-http issuer (private networks and tests only).

Existing users. A provider account is identified by the issuer and its subject (sub). To let an existing (password) user sign in with SSO and keep their tokens and chats, link them before their first SSO sign-in, where the database is reachable and the OIDC settings are set:

docker compose exec lore node dist/cli.js link-sso alice 3f2a…   # the provider's "sub" of alice
docker compose exec lore node dist/cli.js unlink-sso alice

After changing OIDC_ISSUER, users linked to the old issuer are not found any more; link them again.

Role changes take effect at the next sign-in. Moving a person between OIDC_ADMIN_GROUP and OIDC_LOGIN_GROUP (or out of both) is applied when they next sign in with SSO. An SSO web session lives at most LORE_SSO_SESSION_MAX_HOURS hours (default 24) in total, so a role change or removal from the groups reaches the web UI within that time; their sk_ tokens keep the old role until revoked. Offboarding: remove the person from the groups at the provider and also disable the user on the Users page: that ends their web sessions and stops their sk_ tokens (CLI, MCP, scripts) immediately; the group change alone does not touch tokens. There is no last-admin guard: if nobody is in OIDC_ADMIN_GROUP any more, the break-glass LORE_API_KEY still works for the admin API.

Troubleshooting

A failed SSO sign-in returns to /login?loginError=<code>, and the page shows a fixed message:

loginError

Message

Likely cause

invalid_state

The sign-in attempt expired or did not start in this browser.

The attempt is older than 10 minutes, the lore_sso cookie was lost (different host than PUBLIC_URL, blocked cookies), the callback was replayed, or the provider rejected the code exchange (wrong client secret or redirect URI).

not_allowed

Your account is not in a group that may use Lore.

The person is in neither OIDC_LOGIN_GROUP nor OIDC_ADMIN_GROUP, or the provider does not send the groups claim (check OIDC_SCOPES / OIDC_GROUPS_CLAIM).

disabled

Your account is disabled.

An admin disabled the user on the Users page.

idp_unavailable

The identity provider is unavailable right now.

Lore cannot reach OIDC_ISSUER (discovery or token endpoint): network, DNS or TLS; a plain-http issuer needs OIDC_ALLOW_INSECURE=true.

rate_limited

Too many sign-in attempts.

More than 30 sign-in starts from one IP address in 10 minutes.

internal

Sign-in failed.

Anything else; details are in the server log.

If signing in succeeds but you land on the sign-in page again, the session cookie was not kept: open Lore at PUBLIC_URL (same scheme and host). Behind a TLS-terminating proxy set LORE_COOKIE_SECURE=true, and set LORE_TRUST_PROXY (the number of proxy hops, or the proxies' addresses) so that Lore takes the client address from X-Forwarded-For. Without it every request comes from the proxy's address and the per-IP limits (SSO start, CLI login, concurrent password sign-ins) are shared by everyone. Never set it when clients reach Lore directly: they could then choose their own address.

Project keys and namespaces

Each service owns one library, and every library belongs to one namespace (an admin-defined group of libraries, for example per team). Existing libraries are in default; insights is the system namespace for published chats and takes no pushes.

CI pushes with a project key (lpk_…): it belongs to one namespace (optionally to one library of it) and may only push there, including creating a new library inside its namespace on the first push. Pushing to a library of another namespace, or creating a library id that already exists elsewhere, gets 403. A project key cannot read docs, chat, use MCP or the admin API (403 on /api, 401 on MCP). Every push checks the key and the library's namespace again, so a revoked key or a library moved to another namespace stops the next push at once. Only the key's SHA-256 is stored, and it is shown once.

An admin creates namespaces and keys on the Namespaces & keys page, or with the CLI:

lore namespace create payments --name "Payments"
lore namespace assign orders-api payments          # move an existing library
lore project-key create payments --name github-ci  # prints lpk_… once
lore project-key list
lore project-key revoke <id>

Store the key as a CI secret (LORE_PROJECT_KEY below). In the service's pipeline, use the released image — no need to build Lore:

# .github/workflows/docs.yml (in the service repository)
name: docs
on:
  push:
    branches: [main]
jobs:
  push-docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Push docs to Lore
        env:
          LORE_URL: ${{ secrets.LORE_URL }}
          LORE_TOKEN: ${{ secrets.LORE_PROJECT_KEY }}
        run: |
          docker run --rm -v "$PWD/docs:/docs:ro" -e LORE_URL -e LORE_TOKEN \
            ghcr.io/ai-pipes/lore:1 \
            node dist/cli.js push /docs -l orders-api -v "${{ github.sha }}" -b "${{ github.ref_name }}"

The same works anywhere Docker runs (GitLab CI, Jenkins, a laptop). Without Docker, check out this repository and run npm ci && npm run build && node dist/cli.js push .... Admins may also push with their own token; --namespace <id> puts a new library into a namespace (a project key always uses its own).

For feature branches add --no-promote: the version is stored but does not become current; the chat API and MCP tools accept a branch argument to query it.

The document format (frontmatter fields, recommended sections, slug conventions, dependency patterns) is described in docs/usage.md. A minimal document:

---
slug: orders/refund-order
domain: orders
entry_type: handler
handler_class: RefundOrderHandler
http_method: POST
route: /api/v1/orders/{orderId}/refund
summary: Refunds the adoption fees of a delivered order within 14 days
category: specification
links:
  - slug: notifications/send-notification
    library: notifications-service
    type: calls
---

# RefundOrderHandler

## Business Logic

1. The order must be delivered at most 14 days ago.
...

Generating docs (example generator)

Lore stores and serves docs; producing them is up to you. examples/generator is a working example of the generation side: it reads an OpenAPI spec, asks an OpenAI-compatible LLM to write one document per endpoint (interfaces, dependencies, business logic) using the generator/endpoint-doc prompt, writes the files in Lore's format and optionally pushes them.

# Without an API key (or with --dry-run): deterministic skeletons from the spec
npx tsx examples/generator/generate.ts \
  --spec examples/generator/petstore.openapi.json \
  --service petstore-api --out generated-docs/petstore-api --dry-run

# With an LLM, including source files that mention each operationId, and push
OPENAI_API_KEY=sk-... LORE_URL=https://lore.example.com LORE_TOKEN=lpk_... \
npx tsx examples/generator/generate.ts \
  --spec openapi.json --service petstore-api --out generated-docs/petstore-api \
  --source-dir src --model gpt-4o-mini --push --version $(git rev-parse HEAD)

Output from the LLM is validated (the frontmatter must parse and keep the expected slug; links to unknown slugs are dropped); anything invalid falls back to the skeleton. examples/generator/github-workflow.yml is a ready-to-copy workflow that runs a dry run on pull requests and generates + pushes on merge to main.

Chat

The chat UI is served at /. It needs an LLM for written answers (OPENAI_API_KEY, or LLM_BASE_URL + LLM_MODEL for a local OpenAI-compatible server such as Ollama or vLLM). Without one it still runs the search and shows the results panel.

  • Q&A answers only from the retrieved documents and cites each one with a chip. If the docs do not contain the answer it says so.

  • Brainstorm reasons more freely about design and trade-offs, citing only facts taken from documents.

  • Context: pin documents from the results panel to discuss exactly those; the assistant can suggest more documents to pin.

  • Publish: summarises the conversation into an insight document in the insights library (LORE_INSIGHTS_LIBRARY, default insights, created in — or moved into — the system namespace insights at startup; its id is reserved, so no push by anyone may create or write a library with that id in any namespace), with links to the documents the chat cited or pinned, optionally tagged with a related service. Insights live outside service libraries (in the system namespace insights), so lore push never deletes them, and no push route may write there. Every signed-in user may publish their own chats; an insight records its author (user id and display name). Members always publish to insight/<chat id> and cannot overwrite an existing insight; only admins can choose a slug or overwrite.

CLI

The lore CLI (node dist/cli.js, also in the image) talks to the server over HTTP and always authenticates.

lore login --url https://lore.example.com   # opens the browser; approve "Sign in the CLI on <hostname>"
lore whoami                                 # alice (admin) at https://lore.example.com
lore push ./docs -l orders-api -v "$(git rev-parse HEAD)"
lore logout                                 # revokes the token on the server and deletes the file

lore login is a device login: the CLI prints a short code (BCDF-GHJK) and opens the approval page (/cli/login; the CLI opens the server's URL only when it is http(s) on the same origin as the server it signs in to, without a shell — rundll32 url.dll,FileProtocolHandler on Windows — and otherwise just prints <server>/cli/login), where you sign in (password or SSO) and type the code yourself (links never carry it, so nobody can send you a ready-made approval link). The page then shows the hostname the CLI reported (not verified) and the IP address the request came from; approve only a request you just started. The CLI polls every 5 seconds; the request expires after 10 minutes. On approval the server mints one personal token named cli-<hostname> that expires after LORE_CLI_TOKEN_TTL_DAYS days (default 90; visible and revocable on the Tokens page; run lore login again when it expires), and the CLI stores it with the server URL in ~/.config/lore/credentials.json (mode 600, directory 700). lore login --token <sk_… or lpk_…> stores a pasted token instead.

Where a command gets its server and token:

  • the server is --url, else LORE_URL, else the one in the credentials file, else http://localhost:4737;

  • the token is LORE_TOKEN, else the credentials file's — but only when the file is for that same server. Otherwise the command stops with not logged in to <url>, so a stored token is never sent to another server.

In CI set LORE_URL and LORE_TOKEN (a project key). The server checks roles: push needs an admin or a project key of the library's namespace, namespace and project-key need an admin. Server-side commands (serve, mcp, status, link-sso, unlink-sso) use the database directly. The full command reference is in docs/usage.md.

MCP

lore serve exposes MCP on port 4738 at /mcp (Streamable HTTP) and /sse (legacy SSE). Every request needs Authorization: Bearer <token> with a personal sk_ token (create one on the Tokens page, /tokens, which also shows the ready claude mcp add … command) or the admin key LORE_API_KEY. Anything else, including project keys and tokens of disabled users, gets 401.

Tokens run as their owner: chat state they create belongs to that user, and a disabled user's token stops working. Claude Code, with the command the Tokens page shows (LORE_MCP_PUBLIC_URL sets its URL):

claude mcp add --transport http lore http://localhost:4738/mcp \
  --header "Authorization: Bearer sk_..."

Or run it over stdio against the database directly (no HTTP server needed):

npm ci && npm run build
claude mcp add lore -e DATABASE_URL=postgres://lore:lore@localhost:5433/lore \
  -e LORE_EMBEDDING_PROVIDER=hash -- node "$PWD/dist/cli.js" mcp

The full tool list and the HTTP API are in docs/integration.md.

Configuration

All settings are environment variables (see .env.example). An empty value counts as unset.

Variable

Default

Meaning

DATABASE_URL

required

Postgres with the vector extension. Compose sets it.

LORE_API_KEY

required for serve and Compose

Break-glass admin bearer for bootstrap and scripts: pushes to any namespace and uses the admin API, but has no user (no chat). At least 24 characters, not a default value, in production.

LORE_ADMIN_LOGIN, LORE_ADMIN_PASSWORD

unset

The first admin user, created by serve when there are no users yet (password mode). Set both or neither; the password needs at least 12 characters. Ignored in SSO mode.

PUBLIC_URL

unset (required with SSO)

The URL people open Lore at: the SSO redirect URI and the CLI approval link are built from it (else from the request).

OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET

unset

Single sign-on; all three or none. See Single sign-on for these and OIDC_LOGIN_GROUP, OIDC_ADMIN_GROUP, OIDC_SCOPES, OIDC_GROUPS_CLAIM, OIDC_ALLOW_INSECURE.

LORE_COOKIE_SECURE

false (auto when the request is https)

Force the Secure flag on the session cookie, e.g. behind a TLS-terminating proxy.

LORE_TRUST_PROXY

unset (no proxy trusted)

Reverse proxies whose X-Forwarded-For / X-Forwarded-Proto Lore believes: a number of hops (1 for one proxy) or comma-separated proxy IPs/CIDRs. The sign-in limits key on the client address this yields. true is refused.

LORE_DEVICE_COOKIE_SECRET

derived from LORE_API_KEY

Signs the known-device cookies of password sign-in (at least 32 characters). Set it to keep devices known across admin-key rotations.

LORE_SSO_SESSION_MAX_HOURS

24

Absolute lifetime in hours (1-720) of a web session started through SSO, on top of the 7-day sliding window: after it the person must sign in at the identity provider again. Password sessions are not affected.

LORE_CLI_TOKEN_TTL_DAYS

90

Lifetime in days (1-3650) of the cli-<hostname> tokens lore login mints. Tokens created on the Tokens page never expire.

LORE_ALLOWED_ORIGINS

none

Extra browser Origins accepted on state-changing requests (CSRF check), e.g. when a proxy rewrites Host.

LORE_INSIGHTS_LIBRARY

insights

Library that receives published chats.

LORE_PORT

required for serve (4737 in Compose)

HTTP API + chat UI port.

LORE_HOST

0.0.0.0

Interface for the HTTP API.

LORE_MCP_PORT

required for serve (4738 in Compose)

MCP HTTP port.

LORE_MCP_HOST

127.0.0.1

Interface for MCP HTTP. Compose uses 0.0.0.0 inside the container.

LORE_MCP_PUBLIC_URL

http://localhost:<LORE_MCP_PORT>/mcp

The MCP URL clients use, shown in the Tokens page's claude mcp add command.

LORE_MCP_ALLOWED_HOSTS

loopback names when bound to loopback

Comma-separated accepted Host values (DNS-rebinding protection).

LORE_MCP_ALLOWED_ORIGINS

none

Comma-separated browser Origins allowed to call MCP.

LORE_STORAGE_PATH

./storage

Directory for uploaded images (content-addressed).

LORE_EMBEDDING_PROVIDER

none (hash in Compose)

openai, ollama, hash (offline, deterministic, lexical only) or none (full-text only).

OPENAI_API_KEY

unset

Embeddings for openai; also the LLM key if LLM_API_KEY is unset.

LORE_OLLAMA_URL

http://localhost:11434

Ollama server for ollama embeddings.

LORE_OLLAMA_MODEL

nomic-embed-text

Ollama embedding model. The vector column is 1536-dimensional, so the model must produce 1536-dimensional vectors.

LORE_VECTOR_MIN_SCORE

0.2 for hash, 0 otherwise

Minimum cosine similarity for a vector hit.

LORE_SEARCH_DROP_FACTOR

0.80

In FTS-only or vector-only mode, drop results below top score × factor.

LLM_API_KEY

OPENAI_API_KEY

Key of the OpenAI-compatible chat endpoint.

LLM_BASE_URL

OpenAI

Base URL of an OpenAI-compatible endpoint (e.g. http://localhost:11434/v1). Setting it alone enables the LLM.

LLM_MODEL

gpt-4o-mini

Chat / enrichment model.

LORE_PROMPTS_DIR

unset

Directory with prompt overrides (see below).

LORE_DEFAULT_PROMPTS_DIR

./prompts or next to dist/

Location of the shipped templates.

LORE_PROMPT_VAR_<NAME>

see below

Global prompt variable <name>.

LORE_VENDOR_DIR

dist/vendor or node_modules

Location of the bundled UI libraries.

LORE_MIGRATIONS_DIR

./drizzle or next to dist/

Location of the SQL migrations.

LOG_LEVEL

info

Log level when NODE_ENV=production.

NODE_ENV

unset

production for JSON logs; test enables a reset endpoint used by e2e tests — never set it on a real server.

LORE_URL

the credentials file's, else http://localhost:4737

CLI: server URL.

LORE_TOKEN

the credentials file's (same server only)

CLI: token (sk_… personal, lpk_… project key in CI).

GIT_BRANCH

unset

CLI: default for lore push --branch.

GENERATOR_MODEL

LLM_MODEL

Example generator: model name.

Removed: LORE_READ_KEY (reading needs a signed-in user or a token) and LORE_MCP_API_KEY (MCP checks tokens against the database); the server warns at startup if either is still set.

Customizing prompts

Every LLM prompt is a Markdown template under prompts/, addressed by its path without .md. Point LORE_PROMPTS_DIR at a directory with files of the same relative names to override them; files you do not provide fall back to the defaults.

mkdir -p my-prompts/chat
cp prompts/chat/system.md my-prompts/chat/system.md   # edit it
LORE_PROMPTS_DIR=$PWD/my-prompts npm run dev

With Compose, mount the directory and set the variable in a docker-compose.override.yml:

services:
  lore:
    volumes: ["./my-prompts:/prompts:ro"]
    environment: { LORE_PROMPTS_DIR: /prompts }

Templates use {{variable}}. Substitution is a single pass: inserted values (for example document content) are never re-scanned. Unknown variables are left as they are and logged once. A leading <!-- ... --> comment is stripped, so you can document a template in place.

Global variables, available in every template, set with LORE_PROMPT_VAR_<NAME> or a variables.json (flat JSON object) in LORE_PROMPTS_DIR:

Variable

Default

assistant_name

Lore

example_library

orders-api

example_slug

orders/refund-create

example_dotted

derived: orders-api.orders.refund-create

example_related_slug

orders/refund-webhook

example_entity

orders

example_queue

order-events

Prompts:

Name

Used for

Call variables

chat/system

System message, Q&A mode

—

chat/context

System message when documents are pinned

—

chat/brainstorm

System message, brainstorm mode

—

chat/tool-get-document

Description of the chat get_document tool

—

chat/tool-list-libraries

Description of the chat list_libraries tool

—

search/extract-terms

Turning a question into English search terms

— (question is the user message)

publish/summary

Summarising a chat into an insight document

— (transcript is the user message)

enrichment/links

Extracting links between documents after a push

slug, domain, summary, content, known_slugs, link_types

generator/endpoint-doc

Example generator, one document per endpoint

service, http_method, route, operation_id, slug, domain, entry_type, handler_class, operation, components, known_slugs, source_code

mcp/<tool>

MCP tool descriptions: ask_lore, search_docs, get_document, list_libraries, list_endpoints, get_library_overview, find_related_docs, find_by_data_source, find_by_external_dependency, get_service_topology, get_document_diff

—

The chat relies on the [ref:library_id/slug] and [suggest:library_id.slug] marker formats and on the section names Lore puts into the context (## Relevant documentation, ## Full document content (auto-fetched), ## Pinned documentation); keep them when you rewrite the chat prompts.

Security

  • Every route has an access policy (src/platform/http/route-policy.ts); a route without one stops the server from starting. Only /health*, /login, /assets/*, the sign-in API and the CLI device-login start/poll are public. Everything else needs a caller: a web session, a bearer sk_ token, a project key lpk_ or the admin key LORE_API_KEY. Anonymous API calls get 401 and pages redirect to /login.

  • Authorization matrix:

    Caller

    Read docs/search

    Chat (own history)

    Push

    Admin (users, namespaces, keys)

    Signed-in member / member sk_ token

    all

    yes

    no

    no

    Admin (session, sk_ incl. CLI login) / LORE_API_KEY

    all

    yes (the admin key has no user: no chat)

    any namespace

    yes

    Project key

    push-related only

    no

    own namespace

    no

    Anonymous

    no (except /login, /health*, static assets)

    no

    no

    no

    Nobody, the admin key included, pushes to the insights namespace; insights are written only by publishing a chat. Creating, listing and revoking personal tokens needs a web session. Creating users (POST /api/users), resetting passwords (POST /api/users/:id/password) and changing roles (role in PATCH /api/users/:id) need an admin session: an admin signed in to the web UI, or the break-glass LORE_API_KEY; an admin's sk_ token (CLI, scripts, MCP) gets 403 there.

  • Hashing. Passwords use scrypt (N=2^15, r=8, p=1, 16-byte salt). Tokens, project keys, session ids and device codes are random (prefix + 32 bytes, base64url) and stored only as SHA-256 hex; nothing secret is stored in plaintext, and new tokens and keys are shown once.

  • Password resets. An admin resetting someone's password ends that person's web sessions and revokes all their personal sk_ tokens (MCP clients, scripts and lore login must be set up again). An admin changing their own password on the Users page must give their current password; wrong ones count against the same per-login lock as sign-in, and the attempt that finds the login locked also ends the caller's web sessions.

  • Sessions. The lore_session cookie is HttpOnly, SameSite=Lax, Path=/, Secure over https or with LORE_COOKIE_SECURE=true, and lasts 7 days, sliding; a session started through SSO also ends 24 hours after sign-in (LORE_SSO_SESSION_MAX_HOURS). Signing out or disabling the user ends the session. Tokens of disabled users are rejected on /api and MCP.

  • Project-key scope. A project key pushes only to libraries of its namespace (or its one library) and reads only what lore push needs; each request re-checks the key and the library's namespace, so revoking a key or moving a library takes effect on the next request.

  • Rate limits (password sign-in follows OWASP's "device cookies" model). Every attempt is counted before the password is checked, so concurrent guesses cannot overshoot a limit; a successful sign-in gives its attempt back (the limits count failures); every refusal is 429 with Retry-After and runs no KDF.

    • Per login: 10 failures in 15 minutes lock the login for 15 minutes for every client — even with the right password — except known devices. The counter is keyed by an HMAC of the normalized login, so an unknown login is limited exactly like an existing one (and still costs one dummy password verify).

    • Known devices: a successful sign-in sets lore_device, an HMAC-signed cookie (user id, random device id, 90-day expiry; HttpOnly, SameSite=Lax, sent only to /api/auth/login). A browser presenting a valid one for that user is not subject to the login's lock but to its own limit of 10 failures per 15 minutes. The signing key is LORE_DEVICE_COOKIE_SECRET, else derived from LORE_API_KEY (rotating it voids all device cookies); never stored.

    • Per client address: 50 failures across all logins in 15 minutes lock the address (password spraying); this also bounds how many login counters one address can create. At most 4 sign-ins run at once per address. 30 SSO starts and 30 CLI login starts per 10 minutes.

    • Unlock: an admin lifts a login's lock with Unlock sign-in on the Users page or POST /api/users/:id/unlock.

    • The counters live in memory (per process), O(1) per attempt, in three capped maps: keys seen once (an LRU that absorbs floods of fresh keys), keys with a failure on record (a larger map that fresh keys never evict; only expiry or, when it is full as well, its oldest entry, with a warning in the log) and locks (kept until they expire). Expired entries are swept at most once per window. The client address is the TCP peer unless LORE_TRUST_PROXY names the proxy in front.

  • CSRF: API routes accept only JSON (and multipart for uploads); state-changing requests with a cross-site Origin or Sec-Fetch-Site: cross-site are rejected. The SSO login binding cookie lore_sso (SameSite=Lax, 10 minutes) ties the callback to the browser that started it; the CLI approval page shows the hostname and code so a person can spot a phishing attempt.

  • MCP over HTTP requires a bearer token (sk_ or the admin key), binds to 127.0.0.1 by default, sends no CORS headers and validates Host and Origin against allowlists.

  • Chat UI: everything is served by Lore itself — marked, DOMPurify, highlight.js and mermaid are bundled from npm — so the page makes no external requests and runs under default-src 'self' with no inline scripts or styles (frame-ancestors 'none'). Markdown is sanitized with DOMPurify, and citation/suggestion chips are built with DOM APIs from validated ids. Mermaid runs in strict mode; diagram directives (%%{init}%%) are stripped and styles from rendered diagrams are discarded (diagrams use a fixed stylesheet).

  • Images are accepted only as PNG, JPEG, GIF or WebP, detected by file signature (SVG is rejected), and served with X-Content-Type-Options: nosniff and a sandbox CSP.

  • Errors: 5xx responses carry a generic message; details go to the server log.

  • Documents you push are sent to your LLM provider when the chat, enrichment or the generator use them.

  • LORE_API_KEY must be strong: with NODE_ENV=production the server refuses to start with a well-known default or a key shorter than 24 characters; in development it warns. Compose has no default key.

Please report vulnerabilities privately via GitHub security advisories of this repository.

Released images

Tagged releases (vX.Y.Z) publish a multi-arch image (linux/amd64, linux/arm64) to ghcr.io/ai-pipes/lore with tags X.Y.Z, X.Y and X.

docker run -d --name lore -p 127.0.0.1:4737:4737 -p 127.0.0.1:4738:4738 \
  -e DATABASE_URL=postgres://lore:lore@db.example.com:5432/lore \
  -e LORE_API_KEY="$(openssl rand -hex 32)" -e LORE_ADMIN_LOGIN=admin -e LORE_ADMIN_PASSWORD="$(openssl rand -hex 16)" \
  -e LORE_PORT=4737 -e LORE_MCP_PORT=4738 -e LORE_MCP_HOST=0.0.0.0 \
  -e LORE_MCP_ALLOWED_HOSTS=localhost:4738,127.0.0.1:4738 \
  -e LORE_EMBEDDING_PROVIDER=hash -v lore_storage:/app/storage \
  ghcr.io/ai-pipes/lore:1

The image contains the CLI too: docker run --rm ghcr.io/ai-pipes/lore:1 node dist/cli.js --help.

Development

Requirements: Node.js 22, Docker (for Postgres).

npm ci
cp .env.example .env
docker compose up -d postgres
npm run dev                      # tsx, http://localhost:4737
npm run typecheck
npm test                         # unit + integration tests against Postgres, fully offline
npm run test:e2e                 # Playwright (npx playwright install chromium first)
npm run build                    # dist/cli.js (single bundle)
npm run verify                   # typecheck + lint + arch + test (with coverage) + build

Run npm run verify before pushing; CI runs it together with the e2e suite and the Docker build. npm run arch checks the architecture rules with dependency-cruiser (.dependency-cruiser.cjs), and npm test fails if coverage of the modules' domain and application layers drops below 90%. The architecture (a DDD modular monolith), the import rules and the module map are described in CLAUDE.md and docs/adr/0001-modular-monolith.md.

npm test and npm run test:e2e use a separate lore_test database (derived from DATABASE_URL, or TEST_DATABASE_URL), the offline hash embedding provider and no LLM; LLM calls are mocked where they are tested. npm run test:integration runs search-quality checks that are meant for a real embedding provider.

Project layout: src/main.ts (CLI entry), src/cli (client commands: login, logout, whoami, push, images, namespace, project-key), src/contracts (HTTP request/response types), src/platform (HTTP server, MCP, config, DB, server commands), src/modules/catalog (libraries, versions, documents, images, embeddings, diffs, links, topology), src/modules/llm (LLM client, prompts), src/modules/identity (users, sessions, tokens, SSO, device login, the admin key), src/modules/search (search pipeline), src/modules/chat (chat sessions, answers, insights, chat UI), drizzle/ (SQL migrations), prompts/ (prompt templates), examples/ (demo docs, generator).

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Lets AI agents search and read a folder of Markdown documentation at query time, exposing heading-level search, whole-page or section retrieval, and page listing as tools and resources over stdio or Streamable HTTP. Can run as a CLI, a hosted endpoint, or a library inside an existing Node or Next.js site.
    3
    922 npm
    1
    MIT
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables AI clients to search and retrieve up-to-date technical documentation for popular agent frameworks such as langchain, llama-index, autogen, agno, openai-agents-sdk, mcp-doc, camel-ai, and crew-ai by running site-scoped Google searches and extracting text from the resulting pages. It can be deployed locally over the stdio transport or remotely over SSE, and works with clients like Cline, Cursor, and custom Python clients.
    -
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables AI models to search for and retrieve the latest technical documentation from popular agent frameworks (such as langchain, llama-index, autogen, agno, openai-agents-sdk, mcp-doc, camel-ai, and crew-ai) by running site-scoped web searches and extracting the page text. It can be run locally over stdio or deployed remotely over SSE for use with clients like Cursor and Cline.
    -
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables AI clients to query and retrieve current technical documentation for popular agent frameworks such as LangChain, LlamaIndex, AutoGen, Agno, OpenAI Agents SDK, MCP, Camel-AI, and CrewAI through stdio or SSE.
    -