Lore
Optional integration with OpenAI or OpenAI-compatible LLM endpoints to generate written chat answers, turn questions into English search terms, and optionally produce embeddings for semantic search.
Supports OIDC single sign-on with an identity provider, including group-to-role mapping, automatic user creation on first sign-in, and provider-managed user access.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@LoreWhat happens after an order is placed, and which services are involved?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 pushregisters 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-promoteand be queried by branch.Store. Every version is kept.
get_document_diff/GET /api/libraries/:id/diff/:slugshow 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 aninsightdocument 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 .envor 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"
doneThen 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/loginanswers403 sso_only. Existingsk_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_GROUPis an admin, a member ofOIDC_LOGIN_GROUPa 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, elsesub, 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_PASSWORDare ignored (with a warning); the first admin is whoever signs in as a member ofOIDC_ADMIN_GROUP. Starting a sign-in is limited to 30 per IP address per 10 minutes.
Provider setup
Create two groups:
lore-users(may sign in) andlore-admins(may sign in, and are admins).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 (claimgroupsby default).Set
PUBLIC_URL,OIDC_ISSUER,OIDC_CLIENT_IDandOIDC_CLIENT_SECRETin.env(next todocker-compose.yml, which forwards them and the optionalOIDC_*variables below to theloreservice) and recreate the container (docker compose up -d). The startup log saysSign-in: single sign-on via <issuer>orSign-in: password(a partial setting is a configuration error); check withdocker 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:
In Pocket ID, User Groups → create
lore-usersandlore-adminsand add people to them.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 thegroupsclaim into the token when thegroupsscope is requested, which Lore does by default..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=truedocker compose up -d, openhttps://lore.example.com, and sign in with SSO as a member oflore-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 |
| unset (required with SSO) | The URL people open Lore at; the redirect URI is |
| unset | Issuer URL; discovery at |
| unset | Client ID of the confidential client. |
| unset | Client secret. |
|
| Members may sign in (as members). |
|
| Members may sign in and are admins. |
|
| Requested scopes ( |
|
| Claim with the groups: a list of names, or an object whose keys are the names. |
|
| Allow a plain- |
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 aliceAfter 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:
| Message | Likely cause |
| The sign-in attempt expired or did not start in this browser. | The attempt is older than 10 minutes, the |
| Your account is not in a group that may use Lore. | The person is in neither |
| Your account is disabled. | An admin disabled the user on the Users page. |
| The identity provider is unavailable right now. | Lore cannot reach |
| Too many sign-in attempts. | More than 30 sign-in starts from one IP address in 10 minutes. |
| 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
insightdocument in the insights library (LORE_INSIGHTS_LIBRARY, defaultinsights, created in — or moved into — the system namespaceinsightsat 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 namespaceinsights), solore pushnever 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 toinsight/<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 filelore 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, elseLORE_URL, else the one in the credentials file, elsehttp://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 withnot 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" mcpThe 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 |
| required | Postgres with the |
| required for | 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. |
| unset | The first admin user, created by |
| 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). |
| unset | Single sign-on; all three or none. See Single sign-on for these and |
|
| Force the |
| unset (no proxy trusted) | Reverse proxies whose |
| derived from | Signs the known-device cookies of password sign-in (at least 32 characters). Set it to keep devices known across admin-key rotations. |
|
| 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. |
|
| Lifetime in days (1-3650) of the |
| none | Extra browser |
|
| Library that receives published chats. |
| required for | HTTP API + chat UI port. |
|
| Interface for the HTTP API. |
| required for | MCP HTTP port. |
|
| Interface for MCP HTTP. Compose uses |
|
| The MCP URL clients use, shown in the Tokens page's |
| loopback names when bound to loopback | Comma-separated accepted |
| none | Comma-separated browser |
|
| Directory for uploaded images (content-addressed). |
|
|
|
| unset | Embeddings for |
|
| Ollama server for |
|
| Ollama embedding model. The vector column is 1536-dimensional, so the model must produce 1536-dimensional vectors. |
|
| Minimum cosine similarity for a vector hit. |
|
| In FTS-only or vector-only mode, drop results below top score × factor. |
|
| Key of the OpenAI-compatible chat endpoint. |
| OpenAI | Base URL of an OpenAI-compatible endpoint (e.g. |
|
| Chat / enrichment model. |
| unset | Directory with prompt overrides (see below). |
|
| Location of the shipped templates. |
| see below | Global prompt variable |
|
| Location of the bundled UI libraries. |
|
| Location of the SQL migrations. |
|
| Log level when |
| unset |
|
| the credentials file's, else | CLI: server URL. |
| the credentials file's (same server only) | CLI: token ( |
| unset | CLI: default for |
|
| 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 devWith 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 |
|
|
|
|
|
|
| derived: |
|
|
|
|
|
|
Prompts:
Name | Used for | Call variables |
| System message, Q&A mode | — |
| System message when documents are pinned | — |
| System message, brainstorm mode | — |
| Description of the chat | — |
| Description of the chat | — |
| Turning a question into English search terms | — (question is the user message) |
| Summarising a chat into an insight document | — (transcript is the user message) |
| Extracting links between documents after a push |
|
| Example generator, one document per endpoint |
|
| MCP tool descriptions: | — |
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 bearersk_token, a project keylpk_or the admin keyLORE_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_tokenall
yes
no
no
Admin (session,
sk_incl. CLI login) /LORE_API_KEYall
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
insightsnamespace; 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 (roleinPATCH /api/users/:id) need an admin session: an admin signed in to the web UI, or the break-glassLORE_API_KEY; an admin'ssk_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 andlore loginmust 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_sessioncookie is HttpOnly, SameSite=Lax,Path=/,Secureover https or withLORE_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/apiand MCP.Project-key scope. A project key pushes only to libraries of its namespace (or its one library) and reads only what
lore pushneeds; 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
429withRetry-Afterand 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 isLORE_DEVICE_COOKIE_SECRET, else derived fromLORE_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_PROXYnames the proxy in front.
CSRF: API routes accept only JSON (and multipart for uploads); state-changing requests with a cross-site
OriginorSec-Fetch-Site: cross-siteare rejected. The SSO login binding cookielore_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 to127.0.0.1by default, sends no CORS headers and validatesHostandOriginagainst 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: nosniffand 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_KEYmust be strong: withNODE_ENV=productionthe 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:1The 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) + buildRun 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
This server cannot be deployed
Maintenance
Related MCP Connectors
Versioned documentation registry and semantic search for AI tools and coding assistants.
Search @imqueue docs and scaffold typed services & clients from your AI coding agent.
Ingest, manage, and retrieve documents for RAG-powered AI applications
Provide your AI coding tools with token-efficient access to up-to-date technical documentation for…
Related MCP Servers
AlicenseAqualityAmaintenanceLets 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.3922 npm1MIT- FlicenseNot gradedqualityAmaintenanceEnables 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.-
- FlicenseNot gradedqualityAmaintenanceEnables 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.-
- FlicenseNot gradedqualityAmaintenanceEnables 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.-