Skip to main content
Glama

MCP-RAGAnything

Multi-modal RAG service exposing a REST API and MCP server for document indexing and knowledge-base querying, powered by RAGAnything and LightRAG. Two retrieval pathways are available: a graph-based LightRAG pipeline and a classical RAG pipeline using multi-query generation with LLM-as-judge scoring. Files are retrieved from MinIO object storage and indexed into a PostgreSQL-backed knowledge graph. Each project is isolated via its own working_dir.

The service also hosts the MCP server registry — a CRUD API for registering external MCP servers and generating new MCP servers on the fly from OpenAPI/Swagger documents. The registry is persisted in PostgreSQL and rehydrated at startup, so composable-agents and other clients can discover all available MCP servers from a single endpoint. See MCP Server Registry.

Branch feat/dual-auth-rls-llm-peruser introduces a new security model: dual authentication (JWT/OIDC via Logto or per-user API keys shared with composable-agents), Row-Level Security on mcp_servers scoped by user_id, per-user LLM credentials (decrypted from the shared user_llm_settings table), and per-user RAG isolation (metadata-level user_id filter on chunks). See Authentication, Configuration, and Database Schema for the details. The legacy master API_KEY and OPEN_ROUTER_API_KEY for chat/embeddings are deprecated (see Breaking changes).

Architecture

                            Clients
                     (REST / MCP / Claude)
                               |
                 +-------------+-------------+
                 |          FastAPI App        |
                 +-------------+-------------+
                               |
               +---------------+---------------+
               |                               |
        Application Layer            MCP Servers (FastMCP)
        +------------------------------+       |
        | api/                         |   +---+--------+  +--+-----------+  +--+-------------+  +--+----------+
        |   indexing_routes.py         |   | RAGAnything |  | RAGAnything |  | RAGAnything    |  | RAGAnything |
        |   query_routes.py            |   | Query       |  | Files       |  | Classical      |  | Bricks     |
        |   file_routes.py             |   |  /rag/mcp   |  |  /files/mcp |  |  /classical/mcp|  | /bricks/mcp|
        |   health_routes.py           |   +---+--------+  +--+-----------+  +--+-------------+  +--+----------+
        |   classical_indexing_routes   |       |               |                 |                  |
        |   classical_query_routes      |       |               |         classical_index_file   list_bricks_documents
        | use_cases/                   |       |               |         classical_index_folder  read_bricks_document
        |   IndexFileUseCase           |       |               |         classical_query         publish_section_version
        |   IndexFolderUseCase         |
        |   QueryUseCase               |
        |   ClassicalIndexFileUseCase   |
        |   ClassicalIndexFolderUseCase |
        |   ClassicalQueryUseCase       |
         |   ListFilesUseCase           |
         |   ListFoldersUseCase         |
         |   ReadFileUseCase            |
         |   UploadFileUseCase          |
         |   CreateFolderUseCase        |
         |   DeleteFileUseCase          |
         |   DeleteFolderUseCase        |
         |   ListBricksDocumentsUseCase |
        |   ReadBricksDocumentUseCase  |
        |   PublishSectionVersionUseCase|
        | requests/ responses/         |
        +------------------------------+
                 |         |          |
                 v         v          v
      Domain Layer (ports)
      +----------------------------------------------------------+
      | RAGEnginePort  StoragePort  BM25EnginePort              |
      | DocumentReaderPort  VectorStorePort  LLMPort            |
      | BricksApiPort                                            |
      +----------------------------------------------------------+
               |         |          |            |      |
               v         v          v            v      v
      Infrastructure Layer (adapters)
      +----------------------------------------------------------+
       | LightRAGAdapter       MinioAdapter                        |
       | (RAGAnything/         (minio-py)                          |
       |  KreuzbergParser)                                         |
      |                                                            |
      | PostgresBM25Adapter       RRFCombiner                      |
      | (pg_textsearch)            (hybrid+ fusion)                |
      |                                                            |
      | KreuzbergAdapter          LangchainPgvectorAdapter         |
      | (kreuzberg - 91 formats) (langchain-postgres PGVector)    |
      |                                                            |
      | LangchainOpenAIAdapter    BricksApiAdapter                 |
      | (langchain-openai ChatOpenAI)  (httpx, Bricks REST API)   |
      +----------------------------------------------------------+
               |         |          |            |      |
               v         v          v            v      v
         PostgreSQL        MinIO       Kreuzberg    OpenAI-compatible    Bricks API
         (pgvector +     (object     (document     (LLM API)          (analyse.bricks.co
          Apache AGE      storage)    extraction)                      + section-versions)
          pg_textsearch)

Related MCP server: rag-mcp

Prerequisites

  • Python 3.13+

  • Docker and Docker Compose

  • A Logto instance (for JWT/OIDC authentication) — optional if you only use per-user API keys

  • Per-user LLM credentials configured in composable-agents (table user_llm_settings), or an OpenRouter API key as fallback for local dev / Kreuzberg VLM

  • The soludev-compose-apps/bricks/ stack for production deployment (provides PostgreSQL, MinIO, and this service)

Quick Start

Production runs from the shared compose stack at soludev-compose-apps/bricks/. The docker-compose.yml in this repository is for local development only.

Local development

# 1. Install dependencies
uv sync

# 2. Start PostgreSQL and MinIO (docker-compose.yml provides Postgres;
#    MinIO must be available separately or added to the compose file)
docker compose up -d postgres

# 3. Configure environment
cp .env.example .env
# Edit .env: set LOGTO_URL + JWT_AUDIENCE for JWT auth, SECRET_ENCRYPTION_KEY
# (shared with composable-agents), and adjust MINIO_HOST / POSTGRES_HOST.
# OPEN_ROUTER_API_KEY is only needed for the VLM and as a fallback when auth
# is disabled (per-user LLM credentials take precedence on authenticated requests).

# 4. Run the server
uv run python src/main.py

The API is available at http://localhost:8000. Swagger UI at http://localhost:8000/docs.

Production (soludev-compose-apps)

cd soludev-compose-apps/bricks/
docker compose up -d

This starts all brick services including raganything-api, postgres, and minio.

Authentication

The service supports dual authentication on every protected REST and MCP endpoint:

  1. JWT (OIDC)Authorization: Bearer <jwt> issued by a Logto instance. The token is validated against the Logto JWKS, the aud claim is checked against JWT_AUDIENCE, and the user identity is extracted from the JWT claims.

  2. Per-user API keyX-API-Key: <user-api-key> stored in the shared api_keys table (owned by composable-agents, see Shared tables). The key is looked up, the bound user_id becomes the request identity.

The middleware accepts either header on the same request. If both are present, JWT takes precedence. The resolved user_id is stored in a request-scoped contextvar (current_user_id) and is used for:

Enabling authentication

Both modes are active as soon as the relevant env vars are set:

LOGTO_URL=https://logto.example.com
JWT_AUDIENCE=https://raganything.soludev.tech
SECRET_ENCRYPTION_KEY=<fernet-key-shared-with-composable-agents>

SECRET_ENCRYPTION_KEY must be the same Fernet key as composable-agents, because the api_keys and user_llm_settings tables store encrypted values that this service decrypts (see Shared tables).

Disabling authentication

Leave LOGTO_URL and JWT_AUDIENCE empty to disable JWT validation. Per-user API keys still work if rows exist in api_keys. For local development with no auth at all, leave everything empty — health endpoints are always public.

REST usage

# JWT (Logto OIDC)
curl -H "Authorization: Bearer ${JWT}" \
     -H "Content-Type: application/json" \
     -X POST http://localhost:8000/api/v1/classical/query \
     -d '{"working_dir": "project-alpha", "query": "test"}'

# Per-user API key
curl -H "X-API-Key: ${USER_API_KEY}" \
     -H "Content-Type: application/json" \
     -X POST http://localhost:8000/api/v1/classical/query \
     -d '{"working_dir": "project-alpha", "query": "test"}'

Health endpoints (/api/v1/health, /api/v1/health/live) remain public regardless of the auth configuration.

MCP usage

The McpApiKeyMiddleware accepts both X-API-Key and Authorization: Bearer via FastMCP's get_http_headers. MCP clients must send one of them in their HTTP transport configuration. For composable-agents, add the header to the headers field of each MCP server config:

mcp_servers:
  - name: bricks
    transport: http
    url: https://raganything.soludev.tech/bricks/mcp
    headers:
      Authorization: "Bearer ${LOGTO_USER_TOKEN}"   # or
      # X-API-Key: "${MCP_RAGANYTHING_API_KEY}"
  - name: files
    transport: http
    url: https://raganything.soludev.tech/files/mcp
    headers:
      Authorization: "Bearer ${LOGTO_USER_TOKEN}"
  - name: classical
    transport: http
    url: https://raganything.soludev.tech/classical/mcp
    headers:
      Authorization: "Bearer ${LOGTO_USER_TOKEN}"

Protected endpoints

Layer

Protected

Public

REST

/api/v1/files/*, /api/v1/files, /api/v1/classical/*, /api/v1/file/*, /api/v1/folder/*, /api/v1/query, /api/v1/mcp/servers*

/api/v1/health, /api/v1/health/live

MCP

tools/call, tools/list (all 4 servers + generated servers)

initialize

Per-user LLM credentials

When the current_user_id contextvar is set (i.e. the request was authenticated), the LLM/embeddings factories resolve credentials from the shared user_llm_settings table (owned by composable-agents) instead of the static LLMConfig env vars:

  • get_chat_llm_for_user(current_user_id) — returns a ChatOpenAI configured with the user's decrypted api_key, base_url, and model.

  • get_embedding_for_user(current_user_id) — returns an OpenAIEmbeddings configured with the user's credentials.

  • get_vector_store_for_user(current_user_id, working_dir) — returns a PGVectorStore bound to the user's embedding model + dimension.

Decryption is done by AsyncpgUserLlmReader using the existing FernetSecretCipher and the shared SECRET_ENCRYPTION_KEY. If the user has no row in user_llm_settings, the request returns 422 LlmNotConfiguredError with a message telling the user to configure their LLM credentials in composable-agents.

When current_user_id is None (e.g. local dev with auth disabled), the factories fall back to the static LLMConfig env vars (OPEN_ROUTER_API_KEY, CHAT_MODEL, EMBEDDING_MODEL, etc.). This preserves the legacy local-dev workflow.

Per-user RAG isolation

Classical RAG chunks are tagged with user_id in their langchain_metadata at index time. At query time, the langchain-postgres metadata filter {"user_id": current_user_id} is applied so a user only retrieves their own chunks. This is an application-level filter — RLS is not applied on the dynamic classical_rag_* tables because PGVectorStore uses its own connection pool and the app.user_id GUC is not propagated there (same conclusion as the LangGraph store in composable-agents).

Shared tables

Two tables are owned by composable-agents (created by its Alembic migrations) and read by mcp-raganything:

Table

Owner

Purpose

mcp-raganything access

api_keys

composable-agents

Per-user API keys (hashed) + user_id binding

Read (auth lookup) with SET LOCAL row_security = off

user_llm_settings

composable-agents

Per-user LLM credentials (Fernet-encrypted)

Read (credential resolution) with SET LOCAL row_security = off

mcp-raganything reads these tables as a privileged auth/credential-resolution operation: the asyncpg connection sets SET LOCAL row_security = off for the duration of the lookup, then the connection is returned to the pool. RLS on these tables is enforced by composable-agents for its own writes.

The two services share the same PostgreSQL database (raganything) but use separate Alembic version tables: composable-agents uses alembic_version, mcp-raganything uses raganything_alembic_version (see Database Schema). This lets both services evolve their schemas independently without colliding.

Breaking changes

  • API_KEY (master key) deprecated. The single shared X-API-Key: <master> mode is no longer the primary auth path. The env var is still read for backward compatibility but per-user API keys (from the shared api_keys table) are the recommended replacement. Migrate by creating per-user keys in composable-agents.

  • OPEN_ROUTER_API_KEY deprecated for chat + embeddings. When a request is authenticated (current_user_id set), the per-user user_llm_settings row is used instead. OPEN_ROUTER_API_KEY is still used for the Kreuzberg VLM (vision model OCR in the LightRAG pipeline) because the VLM is not user-scoped — this is a documented limitation. To fully deprecate OPEN_ROUTER_API_KEY, the VLM must be made user-scoped too (future work).

  • mcp_servers is now per-user. A user can only see, create, update, and delete their own MCP servers. Cross-user create with the same name returns 409 Conflict (BUG-001 fix: ON CONFLICT DO UPDATE is scoped by user_id, so a same-name server from another user is not silently overwritten).

API Reference

Base path: /api/v1

Health

# Health check
curl http://localhost:8000/api/v1/health

Response:

{"message": "RAG Anything API is running"}

Indexing

Both indexing endpoints accept JSON bodies and run processing in the background. Files are downloaded from MinIO, not uploaded directly.

Index a single file

Downloads the file identified by file_name from the configured MinIO bucket, then indexes it into the RAG knowledge graph scoped to working_dir.

curl -X POST http://localhost:8000/api/v1/file/index \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "file_name": "project-alpha/report.pdf",
    "working_dir": "project-alpha"
  }'

Response (202 Accepted):

{"status": "accepted", "message": "File indexing started in background"}

Field

Type

Required

Description

file_name

string

yes

Object path in the MinIO bucket

working_dir

string

yes

RAG workspace directory (project isolation)

Index a folder

Lists all objects under the working_dir prefix in MinIO, downloads them, then indexes the entire folder.

curl -X POST http://localhost:8000/api/v1/folder/index \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "working_dir": "project-alpha",
    "recursive": true,
    "file_extensions": [".pdf", ".docx", ".txt"]
  }'

Response (202 Accepted):

{"status": "accepted", "message": "Folder indexing started in background"}

Field

Type

Required

Default

Description

working_dir

string

yes

--

RAG workspace directory, also used as the MinIO prefix

recursive

boolean

no

true

Process subdirectories recursively

file_extensions

list[string]

no

null (all files)

Filter by extensions, e.g. [".pdf", ".docx", ".txt"]

Supported Document Formats

The service automatically detects and processes the following document formats through the RAGAnything parser:

Format

Extensions

Notes

PDF

.pdf

Includes OCR support (English + French via Tesseract)

Microsoft Word

.docx

Microsoft PowerPoint

.pptx

Microsoft Excel

.xlsx

HTML

.html, .htm

Plain Text

.txt, .text, .md

UTF-8, UTF-16, ASCII supported; converted to PDF via ReportLab

Quarto Markdown

.qmd

Quarto documents

R Markdown

.Rmd, .rmd

R Markdown files

Images

.png, .jpg, .jpeg, .gif, .webp, .bmp, .tiff, .tif

Vision model processing (if enabled)

Note: File format detection is automatic. No configuration is required to specify the document type. The service will process any supported format when indexed. All document and image formats are supported out-of-the-box when installed with raganything[all].

File Browsing & Reading

Browse and read files directly from MinIO without indexing them into the RAG knowledge base. Powered by Kreuzberg for document text extraction (91 file formats).

List files

# List all files in the bucket
curl -H "Authorization: Bearer ${JWT}" \
     http://localhost:8000/api/v1/files/list

# List files under a specific prefix
curl -H "Authorization: Bearer ${JWT}" \
     "http://localhost:8000/api/v1/files/list?prefix=documents/&recursive=true"

Response (200 OK):

[
  {"object_name": "documents/report.pdf", "size": 1024, "last_modified": "2026-01-01 00:00:00+00:00"},
  {"object_name": "documents/notes.txt", "size": 512, "last_modified": "2026-01-02 00:00:00+00:00"}
]

Parameter

Type

Default

Description

prefix

string

""

MinIO prefix to filter files by

recursive

boolean

true

List files in subdirectories

Upload a file

Uploads a file directly to the MinIO bucket. The file is stored at {prefix}{filename}. This endpoint does not index the file — use the POST /file/index endpoint after uploading to add it to the RAG knowledge base.

Allowed file types: .pdf, .txt, .docx, .xlsx, .pptx, .md, .csv, .png, .jpg, .jpeg, .gif, .webp, .svg, .bmp, .html, .xml, .json, .rtf, .odt, .ods Maximum file size: 50 MB

curl -X POST http://localhost:8000/api/v1/files/upload \
  -H "Authorization: Bearer ${JWT}" \
  -F "file=@report.pdf" \
  -F "prefix=documents/"

Response (201 Created):

{"object_name": "documents/report.pdf", "size": 2048, "message": "File uploaded successfully"}

Field

Type

Required

Default

Description

file

file

yes

--

The file to upload (multipart form)

prefix

string

no

""

MinIO prefix (folder path). Must be a relative path

Error responses:

Status

Condition

413

File exceeds 50 MB limit

422

Invalid prefix (path traversal/absolute), disallowed file type, or missing file

Read a file

Downloads the file from MinIO, extracts its text content using Kreuzberg, and returns the result. Supports 91 file formats including PDF, Office documents, images, and HTML.

curl -X POST http://localhost:8000/api/v1/files/read \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{"file_path": "documents/report.pdf"}'

Response (200 OK):

{
  "content": "Extracted text from the document...",
  "metadata": {"format_type": "pdf", "mime_type": "application/pdf"},
  "tables": [{"markdown": "| Header | Value |\n|---|---|\n| A | 1 |"}]
}

Field

Type

Description

file_path

string

Required. File path in the MinIO bucket (relative, no .. or absolute paths)

Error responses:

Status

Condition

404

File not found in MinIO

422

Unsupported file format or invalid path (path traversal, absolute path)

Create a folder

Creates a folder marker in the MinIO bucket by writing a 0-byte object whose object name ends with a trailing /. The folder is purely a prefix — MinIO does not have a real folder concept. This endpoint does not index anything.

curl -X POST http://localhost:8000/api/v1/files/folders \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{"prefix": "documents/reports/"}'

Response (201 Created):

{"message": "Folder created", "prefix": "documents/reports/"}

Field

Type

Required

Default

Description

prefix

string

yes

--

Folder prefix to create. Must be a relative path; a trailing / is preserved

Error responses:

Status

Condition

422

Missing, absolute, or path-traversing prefix

Delete a file (cascade)

Deletes a single object from the MinIO bucket and removes its corresponding vectors from the pgvector store. The object_name and working_dir are both required query parameters.

Deletion is performed in two steps, strictly in order:

  1. MinIO first — the object is removed from the storage bucket.

  2. pgvector second — vectors associated with the file (scoped to working_dir) are deleted from the vector store.

If the MinIO deletion fails, the pgvector store is not touched, ensuring no orphaned vectors are created from a failed storage operation.

curl -X DELETE "http://localhost:8000/api/v1/files?object_name=documents/report.pdf&working_dir=project-alpha" \
  -H "Authorization: Bearer ${JWT}"

Response (200 OK):

{"message": "File deleted", "object_name": "documents/report.pdf"}

Parameter

Type

Required

Default

Description

object_name

string

yes

--

Object path to delete. Must be a relative path within the bucket

working_dir

string

yes

--

RAG workspace directory (project isolation). Used to scope the pgvector deletion

Error responses:

Status

Condition

422

Missing, absolute, or path-traversing object_name or working_dir

Delete a folder (recursive, cascade)

Deletes all objects whose object names start with prefix from the MinIO bucket and removes all corresponding vectors from the pgvector store. The deletion is recursive and permanent — there is no confirmation beyond the API call. A trailing / is preserved so that a prefix like documents/ does not match documents-archive/.

The prefix is automatically used as the working_dir for the pgvector deletion — no separate working_dir parameter is needed on this endpoint.

Deletion is performed in two steps, strictly in order:

  1. MinIO first — all objects under the prefix are removed from the storage bucket.

  2. pgvector second — all vectors matching the prefix are deleted from the vector store.

If the MinIO deletion fails, the pgvector store is not touched.

curl -X DELETE "http://localhost:8000/api/v1/files/folders?prefix=documents/reports/" \
  -H "Authorization: Bearer ${JWT}"

Response (200 OK):

{"message": "Folder deleted", "prefix": "documents/reports/"}

Parameter

Type

Required

Default

Description

prefix

string

yes

--

Folder prefix to delete. Must be a relative path; trailing / is preserved. Also used as the working_dir for pgvector cleanup

Error responses:

Status

Condition

422

Missing, absolute, or path-traversing prefix

Path traversal protection

All delete routes (DELETE /files, DELETE /files/folders) and the create-folder route (POST /files/folders) validate the supplied path before touching storage. The validation rules are shared with the existing read/upload/list endpoints:

  • The value must not be empty.

  • It must be a relative path (no leading /, no drive prefixes).

  • After normalization it must not resolve to ., .., start with ../, or contain a /../ segment.

Any violation returns 422 Unprocessable Entity with a FileValidationError body. The object_name / prefix is normalized (backslashes converted to /, trailing slashes preserved) before being forwarded to the StoragePort.

Query

Query the indexed knowledge base. The RAG engine is initialized for the given working_dir before executing the query.

curl -X POST http://localhost:8000/api/v1/query \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "working_dir": "project-alpha",
    "query": "What are the main findings of the report?",
    "mode": "naive",
    "top_k": 10
  }'

Response (200 OK):

{
  "status": "success",
  "message": "",
  "data": {
    "entities": [],
    "relationships": [],
    "chunks": [
      {
        "reference_id": "...",
        "content": "...",
        "file_path": "...",
        "chunk_id": "..."
      }
    ],
    "references": []
  },
  "metadata": {
    "query_mode": "naive",
    "keywords": null,
    "processing_info": null
  }
}

Field

Type

Required

Default

Description

working_dir

string

yes

--

RAG workspace directory for this project

query

string

yes

--

The search query

mode

string

no

"naive"

Search mode: naive, local, global, hybrid, hybrid+, mix, bm25, bypass

BM25 query mode

Returns results ranked by PostgreSQL full-text search using pg_textsearch. Each chunk includes a score field with the BM25 relevance score.

curl -X POST http://localhost:8000/api/v1/query \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "working_dir": "project-alpha",
    "query": "quarterly revenue growth",
    "mode": "bm25",
    "top_k": 10
  }'

Response (200 OK):

{
  "status": "success",
  "message": "",
  "data": {
    "entities": [],
    "relationships": [],
    "chunks": [
      {
        "chunk_id": "abc123",
        "content": "Quarterly revenue grew 12% year-over-year...",
        "file_path": "reports/financials-q4.pdf",
        "score": 3.456,
        "metadata": {}
      }
    ],
    "references": []
  },
  "metadata": {
    "query_mode": "bm25",
    "total_results": 10
  }
}

Hybrid+ query mode

Runs BM25 and vector search in parallel, then merges results using Reciprocal Rank Fusion (RRF). Each chunk includes bm25_rank, vector_rank, and combined_score fields.

curl -X POST http://localhost:8000/api/v1/query \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "working_dir": "project-alpha",
    "query": "quarterly revenue growth",
    "mode": "hybrid+",
    "top_k": 10
  }'

Response (200 OK):

{
  "status": "success",
  "message": "",
  "data": {
    "entities": [],
    "relationships": [],
    "chunks": [
      {
        "chunk_id": "abc123",
        "content": "Quarterly revenue grew 12% year-over-year...",
        "file_path": "reports/financials-q4.pdf",
        "score": 0.0328,
        "bm25_rank": 1,
        "vector_rank": 3,
        "combined_score": 0.0328,
        "metadata": {}
      }
    ],
    "references": []
  },
  "metadata": {
    "query_mode": "hybrid+",
    "total_results": 10,
    "rrf_k": 60
  }
}

The combined_score is the sum of bm25_score and vector_score, each computed as 1 / (k + rank). Results are sorted by combined_score descending. A chunk that appears in both result sets will have a higher combined score than one that appears in only one.


Classical RAG Pipeline

A second retrieval pathway alongside the graph-based LightRAG. Classical RAG uses a straightforward chunk → embed → retrieve flow with two quality-enhancing techniques: multi-query generation and LLM-as-judge relevance scoring. It stores chunks in dedicated PGVector tables (one per working_dir) and does not build a knowledge graph.

How it works

  1. Indexing — A file is downloaded from MinIO, text is extracted via Kreuzberg (with chunking), and each chunk is embedded and stored in a PGVector table.

  2. Querying — The LLM generates N alternative phrasings of the user query (multi-query), similarity search runs for each variation, results are deduplicated by chunk_id, then an LLM judge scores each chunk's relevance on a 0–10 scale. Chunks below the relevance threshold are discarded; the rest are returned sorted by score.

Classical Indexing

Both classical indexing endpoints accept JSON bodies and run processing in the background.

Index a single file (classical)

Downloads the file from MinIO, extracts text with Kreuzberg chunking, and embeds the chunks into a PGVector table scoped to working_dir.

curl -X POST http://localhost:8000/api/v1/classical/file/index \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "file_name": "project-alpha/report.pdf",
    "working_dir": "project-alpha",
    "chunk_size": 1000,
    "chunk_overlap": 200
  }'

Response (202 Accepted):

{"status": "accepted", "message": "File indexing started in background"}

Field

Type

Required

Default

Description

file_name

string

yes

--

Object path in the MinIO bucket

working_dir

string

yes

--

RAG workspace directory (project isolation)

chunk_size

integer

no

1000

Max characters per chunk (100–10000)

chunk_overlap

integer

no

200

Overlap characters between chunks (0–2000)

Index a folder (classical)

Lists all objects under the working_dir prefix in MinIO, downloads them, and indexes each file into the PGVector table.

curl -X POST http://localhost:8000/api/v1/classical/folder/index \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "working_dir": "project-alpha",
    "recursive": true,
    "file_extensions": [".pdf", ".docx", ".txt"],
    "chunk_size": 1000,
    "chunk_overlap": 200
  }'

Response (202 Accepted):

{"status": "accepted", "message": "Folder indexing started in background"}

Field

Type

Required

Default

Description

working_dir

string

yes

--

RAG workspace directory, also used as the MinIO prefix

recursive

boolean

no

true

Process subdirectories recursively

file_extensions

list[string]

no

null (all files)

Filter by extensions, e.g. [".pdf", ".docx", ".txt"]

chunk_size

integer

no

1000

Max characters per chunk (100–10000)

chunk_overlap

integer

no

200

Overlap characters between chunks (0–2000)

Classical Query

Query the classical RAG pipeline. Supports two modes: vector (default) and hybrid (BM25 + vector via Reciprocal Rank Fusion).

Vector mode (default)

The LLM generates query variations, runs vector similarity search for each, deduplicates results, then scores and filters them with an LLM judge.

curl -X POST http://localhost:8000/api/v1/classical/query \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "working_dir": "project-alpha",
    "query": "What are the main findings of the report?",
    "top_k": 10,
    "num_variations": 3,
    "relevance_threshold": 5.0,
    "mode": "vector"
  }'

Hybrid mode

Runs BM25 full-text search and multi-query vector search in parallel, merges results using Reciprocal Rank Fusion (RRF), then scores with an LLM judge. Chunks include bm25_score, vector_score, and combined_score fields.

curl -X POST http://localhost:8000/api/v1/classical/query \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "working_dir": "project-alpha",
    "query": "What are the main findings of the report?",
    "mode": "hybrid"
  }'

Response (200 OK):

{
  "status": "success",
  "message": "",
  "queries": [
    "What are the main findings of the report?",
    "What key results does the report present?",
    "Summarize the primary conclusions from the report"
  ],
  "chunks": [
    {
      "chunk_id": "a1b2c3d4-...",
      "content": "The primary finding indicates that...",
      "file_path": "project-alpha/report.pdf",
      "relevance_score": 8.5,
      "metadata": {"chunk_index": 0},
      "bm25_score": 0.0164,
      "vector_score": 0.0164,
      "combined_score": 0.0328
    }
  ],
  "mode": "hybrid"
}

If BM25 is unavailable (BM25_ENABLED=false or pg_textsearch extension missing), hybrid mode falls back to vector mode and logs a warning.

Field

Type

Required

Default

Description

working_dir

string

yes

--

RAG workspace directory for this project

query

string

yes

--

The search query

top_k

integer

no

10

Maximum chunks to retrieve per query variation (1–100)

num_variations

integer

no

3

Number of LLM-generated query variations (1–10)

relevance_threshold

float

no

5.0

Minimum LLM judge score (0–10) to include a chunk

mode

string

no

"vector"

Query mode: vector (vector-only) or hybrid (BM25+vector RRF)

LightRAG vs Classical RAG

Aspect

LightRAG (graph-based)

Classical RAG

Storage

Apache AGE knowledge graph + pgvector

PGVector tables only

Indexing

Builds entity/relationship graph

Chunk + embed only

Query modes

naive, local, global, hybrid, hybrid+, mix, bm25, bypass

vector (multi-query + LLM judge), hybrid (BM25+vector RRF)

Project isolation

Shared graph per working_dir

Separate PG table per working_dir

Best for

Complex reasoning, relationship traversal

Straightforward document Q&A, simpler setup


MCP Servers

The service exposes four MCP servers, all using streamable HTTP transport:

RAGAnythingQuery — /rag/mcp

Query-focused tools for searching the indexed knowledge base.

Tool: query_knowledge_base

Parameter

Type

Default

Description

working_dir

string

required

RAG workspace directory for this project

query

string

required

The search query

mode

string

"hybrid"

Search mode: naive, local, global, hybrid, hybrid+, mix, bm25, bypass

top_k

integer

5

Number of chunks to retrieve

Tool: query_knowledge_base_multimodal

Parameter

Type

Default

Description

working_dir

string

required

RAG workspace directory for this project

query

string

required

The search query

multimodal_content

list

required

List of multimodal content items

mode

string

"hybrid"

Search mode

top_k

integer

5

Number of chunks to retrieve

RAGAnythingFiles — /files/mcp

File browsing tools for listing and reading files from MinIO storage.

Tool: list_files

Parameter

Type

Default

Description

prefix

string

""

MinIO prefix to filter files by

recursive

boolean

true

List files in subdirectories

Tool: read_file

Parameter

Type

Default

Description

file_path

string

required

File path in MinIO bucket (e.g. documents/report.pdf)

Downloads the file from MinIO, extracts its text content using Kreuzberg, and returns the extracted text along with metadata and any detected tables.

RAGAnythingBricks — /bricks/mcp

Bricks integration tools for accessing project documents from the Bricks platform and publishing structured section versions.

Tool: list_bricks_documents

Parameter

Type

Default

Description

project_unique_id

string

required

Bricks project unique identifier

Returns a list of documents for the specified Bricks project, including metadata like file name, MIME type, size, status, and presigned download URLs.

Tool: read_bricks_document

Parameter

Type

Default

Description

file_url

string

required

Presigned S3 URL from list_bricks_documents

Downloads the document from the presigned S3 URL, extracts its text content using Kreuzberg, and returns the extracted text, metadata, and detected tables. No authentication is required — the URL is already signed.

Tool: publish_section_version

Parameter

Type

Default

Description

project_unique_id

string

required

Bricks project unique identifier

section_key

string

required

Section key to publish (e.g. "summary", "analysis")

content

dict

required

Structured content for the section

workflow_id

string

"agent-haiku-files-v1"

Workflow identifier

workflow_name

string

"haiku-files"

Workflow display name

workflow_metadata

dict

null

Additional workflow metadata

Publishes a structured section version back to the Bricks platform. When BRICKS_PUBLISH_DRY_RUN=true (default), the tool returns a preview of the payload without making an API call. Set BRICKS_PUBLISH_DRY_RUN=false to enable real publishing.

Dry-run response example:

{
  "success": true,
  "message": "DRY RUN — no API call made",
  "dry_run": true,
  "payload_preview": {
    "project_unique_id": "abc-123",
    "section_key": "summary",
    "content": {"title": "Analysis Summary"},
    "workflow_id": "agent-haiku-files-v1",
    "workflow_name": "haiku-files",
    "workflow_metadata": {}
  }
}

RAGAnythingClassical — /classical/mcp

Classical RAG tools for indexing and querying without a knowledge graph.

Tool: classical_index_file

Parameter

Type

Default

Description

file_name

string

required

Object path in the MinIO bucket

working_dir

string

required

RAG workspace directory (project isolation)

chunk_size

integer

1000

Max characters per chunk (100–10000)

chunk_overlap

integer

200

Overlap characters between chunks (0–2000)

Tool: classical_index_folder

Parameter

Type

Default

Description

working_dir

string

required

RAG workspace directory, also used as MinIO prefix

recursive

boolean

true

Process subdirectories recursively

file_extensions

list[string]

null (all files)

Filter by extensions, e.g. [".pdf", ".docx"]

chunk_size

integer

1000

Max characters per chunk (100–10000)

chunk_overlap

integer

200

Overlap characters between chunks (0–2000)

Tool: classical_query

Parameter

Type

Default

Description

working_dir

string

required

RAG workspace directory for this project

query

string

required

The search query

top_k

integer

10

Maximum chunks to retrieve per query variation

num_variations

integer

3

Number of LLM-generated query variations (1–10)

relevance_threshold

float

5.0

Minimum LLM judge score (0–10) to include a chunk

mode

string

"vector"

Query mode: vector (vector-only) or hybrid (BM25+vector RRF)

Transport

All MCP servers use streamable HTTP transport exclusively. Connect MCP clients to the mount paths:

http://localhost:8000/rag/mcp          # RAGAnythingQuery
http://localhost:8000/files/mcp        # RAGAnythingFiles
http://localhost:8000/classical/mcp    # RAGAnythingClassical
http://localhost:8000/bricks/mcp       # RAGAnythingBricks

MCP Server Registry

In addition to the four built-in MCP servers above, mcp-raganything owns the MCP server registry — a CRUD service that lets you register external MCP servers (by URL) or generate new MCP servers on the fly from any OpenAPI/Swagger document. Registered servers are persisted in the mcp_servers PostgreSQL table (shared with composable-agents) and rehydrated at startup, so composable-agents (and other clients) only need to know the registry URL to discover and connect to every available MCP server.

This registry was previously hosted in the composable-agents brick; it has been migrated here so that the RAG service is the single owner of MCP server lifecycle (registration, generation, mounting, crash recovery) and the mcp_servers table schema.

Row-Level Security (per user)

Since branch feat/dual-auth-rls-llm-peruser, the mcp_servers table is row-level isolated by user_id:

  • Migration 002 adds the user_id column.

  • Migration 003 runs ALTER TABLE mcp_servers ENABLE ROW LEVEL SECURITY + FORCE ROW LEVEL SECURITY and creates a policy mcp_servers_user_isolation that restricts rows to user_id = current_setting('app.user_id').

McpRegistryStore sets the GUC app.user_id = <current_user_id> on the asyncpg connection before any query, so every read/write is automatically scoped to the authenticated user. The middleware resolves current_user_id from JWT or per-user API key (see Authentication).

BUG-001 fix: the ON CONFLICT (name) DO UPDATE upsert is scoped by user_id. If user A creates a server named weather and user B tries to create another weather, the conflict check only matches A's row through RLS, so B's insert is not a conflict — it creates a new row. If B already has a weather row, the upsert updates B's own row. There is no silent cross-user overwrite. A duplicate-name conflict within the same user returns 409 Conflict.

What it does

  • Register external MCP servers — store a name, transport URL, optional headers and an encrypted auth token. On create/update, the service connects to the remote server via fastmcp.Client, validates reachability, and records the discovered tool_count. Composable-agents reads these entries and connects to the URL at agent build time.

  • Generate MCP servers from OpenAPI specs — point the registry at any OpenAPI 3.0 (or Swagger 2.0) document URL; the service fetches the spec, builds an in-process FastMCP server exposing one tool per operation, and mounts it under /generated/{name}/mcp.

  • Encrypt secrets at rest — auth tokens and sensitive header values are encrypted with Fernet using the shared SECRET_ENCRYPTION_KEY before being written to mcp_servers. They are only decrypted on the /reveal endpoint or when the server is mounted.

  • Crash recovery — on startup the service reads every openapi row from mcp_servers, re-fetches the spec, rebuilds the FastMCP server, and remounts it. External http servers do not need rehydration (the client connects to them lazily), so only openapi rows are rebuilt.

Endpoints

All registry endpoints live under /api/v1/mcp/servers and are protected by the dual-auth middleware (JWT Authorization: Bearer or per-user X-API-Key). All reads/writes are scoped to the authenticated user_id via RLS — a user only sees and manages their own servers.

Method

Path

Description

Success Status

POST

/api/v1/mcp/servers

Create a registered MCP server (external or openapi) scoped to the current user. Returns the masked entry (secrets hidden).

201

POST

/api/v1/mcp/servers/validate

Dry-run validation of a server config without persisting anything. Returns the parsed/mounted result.

200

GET

/api/v1/mcp/servers

List all registered servers owned by the current user (masked).

200

GET

/api/v1/mcp/servers/{name}

Get a single server owned by the current user (masked).

200

GET

/api/v1/mcp/servers/{name}/reveal

Get a single server with plaintext secrets. Use with care.

200

PUT

/api/v1/mcp/servers/{name}

Update a server (URL, headers, auth token, openapi spec). Re-mounts openapi servers.

200

DELETE

/api/v1/mcp/servers/{name}

Delete a server and unmount it if openapi.

204

A GET/PUT/DELETE on a {name} that exists but is owned by another user returns 404 Not Found (RLS hides the row). A POST with a name already used by the same user returns 409 Conflict.

The request body for POST and PUT extends the McpServerConfig shape with two fields:

Field

Type

Default

Description

name

string

required

Unique server name (1–100 chars).

url

string

null

Server URL. Required for source_type="external", omitted for source_type="openapi" (the mounted URL is generated).

headers

dict

{}

HTTP headers sent to the server (upstream auth for openapi).

env

dict

{}

Environment variables for stdio transport.

auth_token

string

null

Bearer auth token (external only). Encrypted at rest.

source_type

"external" | "openapi"

"external"

Origin of the server. openapi triggers spec fetch + FastMCP generation.

openapi_url

string

null

URL of the OpenAPI document. Required when source_type="openapi".

Swagger 2.0 → OpenAPI 3.0 conversion

When source_type="openapi" and the fetched document is a Swagger 2.0 spec (swagger: "2.0"), the service converts it to OpenAPI 3.0 in-process, in pure Python, with no external service call (offline). The converter handles:

  • swagger: "2.0"openapi: "3.0.x"

  • host + basePath + schemesservers[].url

  • definitionscomponents.schemas

  • responses / parameters / securityDefinitionscomponents.*

  • produces / consumes → per-operation requestBody.content / responses.*.content

  • x-... vendor extensions are preserved

After conversion, the resulting OpenAPI 3.0 document is fed to FastMCP to build the generated server. If the document is already OpenAPI 3.0, no conversion is performed. Validation errors (malformed spec, unreachable URL, unsupported version) are returned as 422 responses on /validate and POST.

Generated server mounting & lifecycle

For source_type="openapi" servers, the service:

  1. Fetches the OpenAPI/Swagger document from openapi_url (with optional headers for upstream auth).

  2. Converts Swagger 2.0 → OpenAPI 3.0 if needed (see above).

  3. Builds a FastMCP server exposing one MCP tool per OpenAPI operation (operationId or method+path as the tool name).

  4. Mounts the server at /generated/{name}/mcp using streamable HTTP transport, with proper lifespan management via an AsyncExitStack so that HTTP clients and sessions opened by FastMCP are closed cleanly on shutdown or unmount.

  5. Persists the entry in mcp_servers with url = {GENERATED_MCP_BASE_URL}/generated/{name}/mcp.

The returned url is built from GENERATED_MCP_BASE_URL so that clients outside the container can reach it. Connect an MCP client to:

{GENERATED_MCP_BASE_URL}/generated/{name}/mcp

For local development the default is http://localhost:8020/generated/{name}/mcp. In Docker the default is http://raganything-api:8000/generated/{name}/mcp (set GENERATED_MCP_BASE_URL to the public/ingress URL if clients are external).

Startup rehydration (crash recovery)

On startup, the FastAPI lifespan reads all rows from mcp_servers and, for each row with source_type="openapi", re-fetches the OpenAPI spec, rebuilds the FastMCP server, and remounts it at /generated/{name}/mcp. External http/stdio servers are not rebuilt (the client connects to them lazily on first tool call). If SECRET_ENCRYPTION_KEY is not set, the registry is disabled at startup and a warning is logged — the rest of the RAG service still starts.

Example: register an external server

# JWT (Logto OIDC)
curl -X POST http://localhost:8000/api/v1/mcp/servers \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "name": "weather",
    "source_type": "external",
    "url": "https://weather.example.com/mcp",
    "auth_token": "super-secret"
  }'

# Per-user API key
curl -X POST http://localhost:8000/api/v1/mcp/servers \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ${USER_API_KEY}" \
  -d '{
    "name": "weather",
    "source_type": "external",
    "url": "https://weather.example.com/mcp",
    "auth_token": "super-secret"
  }'

Response (201 Created, secrets masked) — note the tool_count reflects the tools discovered by connecting to the remote server:

{
  "name": "weather",
  "source_type": "external",
  "url": "https://weather.example.com/mcp",
  "auth_token": null,
  "headers": {},
  "tool_count": 3
}

Example: generate a server from an OpenAPI spec

curl -X POST http://localhost:8000/api/v1/mcp/servers \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${JWT}" \
  -d '{
    "name": "petstore",
    "source_type": "openapi",
    "openapi_url": "https://petstore.swagger.io/v2/swagger.json",
    "headers": {"Authorization": "Bearer upstream-token"}
  }'

Response (201 Created) — note the generated url:

{
  "name": "petstore",
  "source_type": "openapi",
  "url": "http://localhost:8020/generated/petstore/mcp",
  "openapi_url": "https://petstore.swagger.io/v2/swagger.json",
  "headers": {"Authorization": "***"}
}

The mounted server is immediately reachable at http://localhost:8020/generated/petstore/mcp and will be rehydrated automatically on the next startup.


Configuration

All configuration is via environment variables, loaded through Pydantic Settings. See .env.example for a complete reference.

Application (AppConfig)

Variable

Default

Description

HOST

0.0.0.0

Server bind address

PORT

8000

Server port

ALLOWED_ORIGINS

["*"]

CORS allowed origins

OUTPUT_DIR

system temp

Temporary directory for downloaded files

UVICORN_LOG_LEVEL

critical

Uvicorn log level

API_KEY

"" (deprecated)

Legacy master shared secret. When non-empty, requests may authenticate with X-API-Key: <API_KEY>. Deprecated in favor of per-user API keys from the shared api_keys table (see Authentication). Still read for backward compatibility but no longer the primary auth path

LOGTO_URL

"" (JWT disabled)

Base URL of the Logto OIDC instance used to validate Authorization: Bearer <jwt> tokens. When empty, JWT authentication is disabled. Example: https://logto.example.com

JWT_AUDIENCE

""

Expected aud claim for JWT validation. Should match the audience configured in Logto for this service (e.g. https://raganything.soludev.tech). Required when LOGTO_URL is set

SECRET_ENCRYPTION_KEY

"" (registry disabled)

Fernet key shared with composable-agents. Used to (a) encrypt MCP registry secrets (auth tokens, sensitive headers) at rest in mcp_servers, and (b) decrypt per-user LLM credentials in user_llm_settings and per-user API keys in api_keys. Generate with python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())". Must be stable across restarts and identical to composable-agents' key. When empty, the MCP server registry and per-user LLM resolution are disabled at startup (the rest of the service still runs)

GENERATED_MCP_BASE_URL

http://localhost:8020

Absolute base URL of this service, used to build the public URL returned for generated (openapi) MCP servers mounted under /generated/{name}/mcp. Set to http://raganything-api:8000 for Docker, or to your public ingress URL for external clients

MCP_TOOL_TIMEOUT

60.0

Timeout (seconds) for connecting to external MCP servers and listing their tools via the FastMcpToolLoader (used by the external-path create/update/validate flows). Increase for slow remote servers

Database (DatabaseConfig)

Variable

Default

Description

POSTGRES_USER

raganything

PostgreSQL user

POSTGRES_PASSWORD

raganything

PostgreSQL password

POSTGRES_DATABASE

raganything

PostgreSQL database name

POSTGRES_HOST

localhost

PostgreSQL host

POSTGRES_PORT

5432

PostgreSQL port

LLM (LLMConfig)

Deprecated for chat + embeddings on authenticated requests. When current_user_id is set (JWT or per-user API key), the per-user user_llm_settings row (decrypted via SECRET_ENCRYPTION_KEY) takes precedence over these env vars. The env vars remain the fallback when current_user_id is None (local dev, auth disabled) and are still required for the Kreuzberg VLM (vision OCR in the LightRAG pipeline), which is not user-scoped — see Per-user LLM credentials and Breaking changes.

Variable

Default

Description

OPEN_ROUTER_API_KEY

--

Deprecated for chat + embeddings (per-user user_llm_settings takes precedence on authenticated requests). Still required for the Kreuzberg VLM (vision OCR) and as the fallback when auth is disabled. OpenRouter API key

OPEN_ROUTER_API_URL

https://openrouter.ai/api/v1

OpenRouter base URL (fallback)

BASE_URL

--

Override base URL (takes precedence over OPEN_ROUTER_API_URL, fallback only)

CHAT_MODEL

openai/gpt-4o-mini

Chat completion model (fallback)

EMBEDDING_MODEL

text-embedding-3-small

Embedding model (fallback)

EMBEDDING_DIM

1536

Embedding vector dimension (fallback)

MAX_TOKEN_SIZE

8192

Max token size for embeddings

VISION_MODEL

openai/gpt-4o

Vision model for image processing (VLM — not user-scoped, always uses OPEN_ROUTER_API_KEY)

RAG (RAGConfig)

Variable

Default

Description

RAG_STORAGE_TYPE

postgres

Storage backend: postgres or local

DOCUMENT_PARSER

kreuzberg

Document parser for LightRAG pipeline: kreuzberg (VLM OCR via OpenRouter), mineru, or paddleocr

COSINE_THRESHOLD

0.2

Similarity threshold for vector search (0.0-1.0)

MAX_CONCURRENT_FILES

1

Concurrent file processing limit

MAX_WORKERS

3

Workers for folder processing

ENABLE_IMAGE_PROCESSING

true

Process images during indexing

ENABLE_TABLE_PROCESSING

true

Process tables during indexing

ENABLE_EQUATION_PROCESSING

true

Process equations during indexing

BM25 (BM25Config)

Variable

Default

Description

BM25_ENABLED

true

Enable BM25 full-text search

BM25_TEXT_CONFIG

english

PostgreSQL text search configuration

BM25_RRF_K

60

RRF constant K for hybrid search (must be >= 1)

When BM25_ENABLED is false or the pg_textsearch extension is not available, hybrid+ mode falls back to naive (vector-only) and bm25 mode returns an error.

Classical RAG (ClassicalRAGConfig)

Variable

Default

Description

CLASSICAL_CHUNK_SIZE

1000

Max characters per chunk (Kreuzberg ChunkingConfig)

CLASSICAL_CHUNK_OVERLAP

200

Overlap characters between chunks

CLASSICAL_NUM_QUERY_VARIATIONS

3

Number of multi-query variations the LLM generates (1–10)

CLASSICAL_RELEVANCE_THRESHOLD

5.0

Minimum LLM judge score (0–10) for a chunk to be included in results

CLASSICAL_TABLE_PREFIX

classical_rag_

Prefix for PGVectorStore table names. Full name: {prefix}{sha256(working_dir)[:16]}

CLASSICAL_LLM_TEMPERATURE

0.0

Temperature for LLM calls (multi-query generation + judge scoring)

CLASSICAL_RRF_K

60

RRF constant K for hybrid BM25+vector search (must be >= 1)

The classical RAG adapters resolve LLM/embeddings per user via get_chat_llm_for_user / get_embedding_for_user / get_vector_store_for_user when current_user_id is set (see Per-user LLM credentials). If the user has no row in user_llm_settings, the request returns 422 LlmNotConfiguredError. When auth is disabled (current_user_id is None), the adapters fall back to the static LLMConfig env vars (OPEN_ROUTER_API_KEY, CHAT_MODEL, EMBEDDING_MODEL, EMBEDDING_DIM). If initialization fails (e.g. missing API key in fallback mode), the classical endpoints return 503 Service Unavailable with a descriptive error. Chunks are tagged with user_id in langchain_metadata at index time and filtered by {"user_id": current_user_id} at query time (see Per-user RAG isolation).

MinIO (MinioConfig)

Variable

Default

Description

MINIO_HOST

localhost:9000

MinIO endpoint (host:port)

MINIO_ACCESS

minioadmin

MinIO access key

MINIO_SECRET

minioadmin

MinIO secret key

MINIO_BUCKET

raganything

Default bucket name

MINIO_SECURE

false

Use HTTPS for MinIO

Bricks API (BricksConfig)

Variable

Default

Description

BRICKS_API_BASE_URL

https://analyse.bricks.co

Bricks platform base URL

BRICKS_API_KEY

--

X-API-Key for Bricks API authentication (publish)

BRICKS_BEARER_TOKEN

--

Bearer token for Bricks API authentication (list documents)

BRICKS_PUBLISH_DRY_RUN

true

When true, publish_section_version returns a payload preview without making an API call

Query Modes

Mode

Description

naive

Vector search only -- fast, recommended default

local

Entity-focused search using the knowledge graph

global

Relationship-focused search across the knowledge graph

hybrid

Combines local + global strategies

hybrid+

Parallel BM25 + vector search using Reciprocal Rank Fusion (RRF). Best of both worlds

mix

Knowledge graph + vector chunks combined

bm25

BM25 full-text search only. PostgreSQL pg_textsearch

bypass

Direct LLM query without retrieval

Development

uv sync                          # Install all dependencies (including dev)
uv run python src/main.py        # Run the server locally
uv run pytest tests/unit -q      # Run unit tests (557 pass on feat/dual-auth-rls-llm-peruser)
uv run ruff check src/           # Lint
uv run ruff format src/          # Format
uv run mypy src/                 # Type checking

QA via docker stack

The soludev-compose-apps/bricks/ stack auto-seeds the shared api_keys and user_llm_settings tables (the same seed data is used by composable-agents), so end-to-end QA of dual-auth + per-user LLM + RAG isolation can be run without manual seeding:

cd soludev-compose-apps/bricks/
docker compose up -d
# Then exercise the API with a JWT or a seeded per-user API key.

Docker (local)

docker compose up -d             # Start Postgres + API
docker compose logs -f raganything-api   # Follow API logs
docker compose down -v           # Stop and remove volumes

Database Schema

mcp-raganything uses Alembic for the mcp_servers table (the MCP server registry). Migrations live in src/alembic/versions/ and run automatically at startup (via asyncio.to_thread(_run_alembic_upgrade) in the FastAPI lifespan). The migration state is tracked in the raganything_alembic_version table, which is separate from composable-agents' alembic_version table so both services can share the same database without colliding.

Migration

Description

001_create_mcp_servers_table

Creates the mcp_servers table (name, transport, url, Fernet-encrypted headers/env/auth_token, tool_count, timestamps, source_type, openapi_url). CREATE TABLE IF NOT EXISTS makes it safe on databases where the table was previously created by composable-agents' legacy migrations 008/009 (now removed from that brick).

002_add_user_id_to_mcp_servers

Adds the user_id column to mcp_servers (nullable for backward compatibility, populated on new writes by McpRegistryStore from current_user_id).

003_enable_rls_mcp_servers

Runs ALTER TABLE mcp_servers ENABLE ROW LEVEL SECURITY + FORCE ROW LEVEL SECURITY and creates the mcp_servers_user_isolation policy restricting rows to user_id = current_setting('app.user_id'). McpRegistryStore sets app.user_id on the asyncpg connection before every query.

The classical RAG tables are not managed by Alembic:

  • Classical RAG tables (classical_rag_*) — created at runtime by LangchainPgvectorAdapter (langchain-postgres PGVectorStore) the first time a collection is indexed. RLS is not applied on these tables (PGVectorStore uses its own connection pool and the app.user_id GUC is not propagated); isolation is enforced at the application level via the {"user_id": current_user_id} langchain-postgres metadata filter (see Per-user RAG isolation).

  • BM25 index — created on demand by ClassicalBM25Adapter._ensure_bm25_index the first time a classical_rag_* table is queried, using CREATE INDEX ... USING bm25(content).

The shared tables api_keys and user_llm_settings are not managed by this service's Alembic — they are created by composable-agents' Alembic. mcp-raganything only reads them with SET LOCAL row_security = off (see Shared tables).

Production requirements

The PostgreSQL server must have the pg_textsearch extension installed and loaded. In production, this requires:

  1. Dockerfile.db builds a custom PostgreSQL image that compiles pg_textsearch from source (along with pgvector and Apache AGE).

  2. docker-compose.yml must configure shared_preload_libraries=pg_textsearch for the bricks-db service. The local dev docker-compose.yml in this repository includes this by default.

  3. ClassicalBM25Adapter checks for the extension at pool-creation time and logs a warning if it is missing; BM25 queries will then fail at runtime. Ensure the database image is built from Dockerfile.db and the shared library is preloaded.

Project Structure

src/
  main.py                           -- FastAPI app, triple MCP mounts, entry point
  config.py                         -- Pydantic Settings config classes
  dependencies.py                   -- Dependency injection wiring
  domain/
    entities/
      indexing_result.py             -- FileIndexingResult, FolderIndexingResult
    ports/
      rag_engine.py                  -- RAGEnginePort (abstract)
       storage_port.py                -- StoragePort (abstract, methods: list, read, upload, create_folder, remove_object, remove_prefix) + FileInfo
      bm25_engine.py                 -- BM25EnginePort (abstract)
      document_reader_port.py        -- DocumentReaderPort (abstract) + DocumentContent
      vector_store_port.py          -- VectorStorePort (abstract) + SearchResult
      llm_port.py                   -- LLMPort (abstract)
      bricks_api_port.py             -- BricksApiPort (abstract) + BricksDocumentInfo + SectionVersionResult
  application/
    api/
      health_routes.py               -- GET /health
      indexing_routes.py              -- POST /file/index, /folder/index
      query_routes.py                 -- POST /query
      file_routes.py                  -- GET /files/list, GET /files/folders, POST /files/read, POST /files/upload, POST /files/folders, DELETE /files, DELETE /files/folders
      classical_indexing_routes.py   -- POST /classical/file/index, /classical/folder/index
      classical_query_routes.py      -- POST /classical/query
      mcp_query_tools.py              -- MCP tools: query_knowledge_base, query_knowledge_base_multimodal
      mcp_file_tools.py               -- MCP tools: list_files, read_file
      mcp_classical_tools.py          -- MCP tools: classical_index_file, classical_index_folder, classical_query
      mcp_bricks_tools.py             -- MCP tools: list_bricks_documents, read_bricks_document, publish_section_version
    requests/
      indexing_request.py            -- IndexFileRequest, IndexFolderRequest
      classical_indexing_request.py  -- ClassicalIndexFileRequest, ClassicalIndexFolderRequest
      classical_query_request.py     -- ClassicalQueryRequest
      query_request.py                -- QueryRequest, MultimodalQueryRequest
      file_request.py                 -- ListFilesRequest, ReadFileRequest
    responses/
      query_response.py              -- QueryResponse, QueryDataResponse
      classical_query_response.py    -- ClassicalQueryResponse, ClassicalChunkResponse
      file_response.py                -- FileInfoResponse, FileContentResponse
    use_cases/
      index_file_use_case.py         -- Downloads from MinIO, indexes single file (LightRAG)
      index_folder_use_case.py       -- Downloads from MinIO, indexes folder (LightRAG)
      query_use_case.py              -- Query with bm25/hybrid+ support
      classical_index_file_use_case.py  -- Classical: download → Kreuzberg chunk → PGVector
      classical_index_folder_use_case.py -- Classical: folder batch index
      classical_query_use_case.py    -- Classical: multi-query + LLM judge + hybrid BM25
      _classical_helpers.py          -- validate_path, build_documents_from_extraction
      list_files_use_case.py          -- Lists files with metadata from MinIO
      list_folders_use_case.py        -- Lists folder prefixes from MinIO
      read_file_use_case.py           -- Reads file from MinIO, extracts content via Kreuzberg
       upload_file_use_case.py          -- Uploads file to MinIO storage
       create_folder_use_case.py        -- Creates a folder marker (0-byte trailing-slash object) in MinIO
       delete_file_use_case.py          -- Deletes a single object from MinIO storage
       delete_folder_use_case.py        -- Recursively deletes all objects under a prefix in MinIO storage
       list_bricks_documents_use_case.py   -- Lists documents from Bricks API
      read_bricks_document_use_case.py    -- Downloads Bricks document via presigned URL, extracts via Kreuzberg
      publish_section_version_use_case.py -- Publishes section version (dry-run aware)
  infrastructure/
    rag/
      lightrag_adapter.py            -- LightRAGAdapter (RAGAnything/LightRAG)
      kreuzberg_raganything_parser.py -- KreuzbergRAGAnythingParser (kreuzberg, custom parser for RAGAnything)
    storage/
      minio_adapter.py               -- MinioAdapter (minio-py client)
    document_reader/
      kreuzberg_adapter.py            -- KreuzbergAdapter (kreuzberg, 91 formats)
    bm25/
      pg_textsearch_adapter.py        -- PostgresBM25Adapter (pg_textsearch, LightRAG tables)
      classical_bm25_adapter.py        -- ClassicalBM25Adapter (pg_textsearch, classical_rag_* tables)
    hybrid/
      rrf_combiner.py                 -- RRFCombiner (Reciprocal Rank Fusion)
    vector_store/
      langchain_pgvector_adapter.py -- LangchainPgvectorAdapter (langchain-postgres PGVectorStore)
    llm/
      langchain_openai_adapter.py    -- LangchainOpenAIAdapter (langchain-openai ChatOpenAI)
    bricks/
      bricks_api_adapter.py           -- BricksApiAdapter (httpx, Bricks REST API + section-versions)
  alembic/
    env.py                            -- Alembic migration environment (async, version_table=raganything_alembic_version)
    versions/
      001_create_mcp_servers_table.py -- mcp_servers table (Fernet-encrypted secrets, source_type, openapi_url)

Deployment on Railway

Railway is a deployment platform that supports Docker-based services with managed PostgreSQL. The service requires PostgreSQL with pgvector and pg_textsearch extensions.

Architecture on Railway

Railway project
├── PostgreSQL (Railway managed plugin — requires pgvector + pg_textsearch)
├── mcp-raganything (Dockerfile deploy)
└── (MinIO — use Railway's external S3-compatible storage or a separate container)

Step-by-step

  1. Create a Railway project and add a PostgreSQL plugin.

  2. Provision pgvector + pg_textsearch:

    • Railway's managed PostgreSQL is based on the standard postgres image without pgvector or pg_textsearch.

    • You need a custom PostgreSQL image. Use the Dockerfile.db from this repo to build a pgvector/pgvector:pg17-based image with pg_textsearch compiled in.

    • Alternatively, deploy the custom PostgreSQL as a Railway service using Dockerfile.db, and disable the managed plugin.

  3. Deploy mcp-raganything:

    • New Service → GitHub Repo → select this repository.

    • Railway detects the Dockerfile automatically.

    • Set the port to 8000 (Railway auto-detects EXPOSE 8000).

  4. Configure environment variables in the Railway dashboard:

    Variable

    Example

    Notes

    POSTGRES_HOST

    roundhouse.proxy.rlwy.net

    Railway PostgreSQL host

    POSTGRES_PORT

    33019

    Railway PostgreSQL port (not 5432)

    POSTGRES_USER

    postgres

    Railway PostgreSQL user

    POSTGRES_PASSWORD

    ********

    Railway PostgreSQL password

    POSTGRES_DATABASE

    railway

    Railway PostgreSQL database name

    OPEN_ROUTER_API_KEY

    sk-or-v1-...

    OpenRouter API key

    OPEN_ROUTER_API_URL

    https://openrouter.ai/api/v1

    OpenRouter base URL

    CHAT_MODEL

    openai/gpt-4o-mini

    Chat model

    EMBEDDING_MODEL

    text-embedding-3-small

    Embedding model

    EMBEDDING_DIM

    1536

    Embedding dimensions

    MINIO_HOST

    minio.xxx.railway.app:9000

    MinIO host (external service)

    MINIO_ACCESS

    minioadmin

    MinIO access key

    MINIO_SECRET

    ********

    MinIO secret key

    MINIO_BUCKET

    raganything

    MinIO bucket name

    MINIO_SECURE

    true

    Use HTTPS in production

    API_KEY

    ""

    Deprecated legacy master key. Prefer per-user API keys from api_keys

    LOGTO_URL

    https://logto.example.com

    Logto OIDC base URL for JWT validation

    JWT_AUDIENCE

    https://raganything.soludev.tech

    Expected aud claim for JWTs

    SECRET_ENCRYPTION_KEY

    ********

    Fernet key shared with composable-agents (encrypts mcp_servers secrets, decrypts api_keys + user_llm_settings)

    ALLOWED_ORIGINS

    ["https://composable-agents.xxx.railway.app"]

    CORS origins

  5. MinIO: Railway does not offer managed MinIO. Options:

    • Deploy MinIO as a separate Railway service (Docker image minio/minio).

    • Use an external S3-compatible service (AWS S3, Cloudflare R2, etc.) and adapt the MinIO config accordingly.

    • Use the soludev-compose-apps stack if you need all services together.

  6. Verify deployment:

    curl https://mcp-raganything.xxx.up.railway.app/api/v1/health
  7. Connect composable-agents to this Railway deployment by pointing agent MCP URLs to the Railway domain and authenticating with either a Logto user JWT (Authorization: Bearer) or a per-user API key from the shared api_keys table (X-API-Key). The legacy MCP_RAGANYTHING_API_KEY env var (master API_KEY) still works but is deprecated.

Notes

  • Railway automatically generates a public domain (e.g. mcp-raganything-production.up.railway.app).

  • The pg_textsearch extension must be available on the PostgreSQL server. If using Railway's managed PostgreSQL, you will need to replace it with a custom image built from Dockerfile.db.

  • Alembic migrations run automatically on startup — no manual migration step is needed.

License

MIT

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    -
    quality
    D
    maintenance
    A RAG service based on FastMCP that enables document indexing and retrieval (keyword/vector search) through the MCP protocol.
    Last updated
  • F
    license
    -
    quality
    B
    maintenance
    Multi-modal RAG service that exposes both REST API and MCP server for document indexing and knowledge-base querying, supporting graph-based LightRAG and classical RAG pipelines with file retrieval from MinIO and PostgreSQL-backed knowledge graph.
    Last updated
    14
  • A
    license
    -
    quality
    B
    maintenance
    A minimal RAG service that exposes a vector index for document retrieval via REST and MCP, allowing querying for relevant document chunks and returning a suggested LLM prompt.
    Last updated
    MIT

View all related MCP servers

Related MCP Connectors

  • Local-first RAG engine with MCP server for AI agent integration.

  • OCR, transcription, file extraction, and image generation for AI agents via MCP.

  • Self-hosted MCP gateway: turn any API, database or MCP server into AI connectors — no code.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SoluDevTech/mcp-raganything'

If you have feedback or need assistance with the MCP directory API, please join our Discord server