Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions rag-mcp-server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# API Key for external LLM access to the MCP server
# Generate with: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
MCP_API_KEY=your-api-key-here

# SMB Configuration (for accessing LAN file shares)
SMB_DEFAULT_USERNAME=guest
SMB_DEFAULT_PASSWORD=
SMB_DEFAULT_DOMAIN=WORKGROUP

# Embedding model (sentence-transformers model name)
EMBEDDING_MODEL=all-MiniLM-L6-v2

# Server hostname (for display in UI)
SERVER_HOSTNAME=BrownserverN5
SERVER_IP=192.168.1.52
10 changes: 10 additions & 0 deletions rag-mcp-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules/
dist/
.env
data/chromadb/*
!data/chromadb/.gitkeep
data/config/*
!data/config/.gitkeep
__pycache__/
*.pyc
.venv/
145 changes: 145 additions & 0 deletions rag-mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# RAG MCP Server for BrownserverN5

A self-hosted document RAG (Retrieval Augmented Generation) system with an MCP (Model Context Protocol) server, designed for deployment on Unraid.

Cloud-based LLMs (Claude, GPT, etc.) connect via the MCP server to search your local documents using semantic similarity - your documents never leave your server.

## Architecture

```
┌─────────────────┐
│ Web UI (:8902) │
│ React + Nginx │
└───────┬──────────┘
┌───────────────┼───────────────┐
│ │
┌────────┴────────┐ ┌─────────┴────────┐
│ Backend (:8900) │ │ MCP Server(:8901)│
│ FastAPI + RAG │◄───────────│ SSE + HTTP │
│ ChromaDB │ │ API Key Auth │
└────────┬────────┘ └──────────────────┘
┌────────┴────────┐
│ SMB Shares (LAN)│
│ 192.168.1.x │
└─────────────────┘
```

**Three Docker services:**

| Service | Internal Port | External Port | Purpose |
|---------|--------------|---------------|---------|
| Backend | 8000 | **8900** | FastAPI + ChromaDB RAG engine |
| MCP Server | 8001 | **8901** | MCP protocol for cloud LLMs |
| Frontend | 80 | **8902** | React management UI |

## Quick Start

### 1. Deploy on Unraid

SSH into your server or use the Unraid terminal:

```bash
cd /mnt/user/appdata # or wherever you keep app data
git clone <this-repo> rag-mcp-server
cd rag-mcp-server

# Copy and edit environment config
cp .env.example .env

# Deploy
chmod +x deploy.sh
./deploy.sh
```

### 2. Open the Web UI

Navigate to `http://192.168.1.52:8902` in your browser.

### 3. Create an API Key

Go to **API Keys** in the sidebar and create a key. Copy it immediately - it's shown only once.

### 4. Upload Documents

Use the **Documents** page to upload files, or use the **SMB Browser** to ingest documents from LAN shares.

### 5. Connect Your LLM

#### Claude Desktop / Claude Code
Add to your MCP config:
```json
{
"mcpServers": {
"rag-documents": {
"url": "http://192.168.1.52:8901/sse",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```

#### Streamable HTTP (alternative)
For clients that support it, use `http://192.168.1.52:8901/mcp` as the endpoint.

## Supported File Types

| Category | Extensions |
|----------|-----------|
| Text | `.txt`, `.md`, `.csv`, `.log`, `.ini`, `.conf`, `.cfg` |
| Code | `.py`, `.js`, `.ts`, `.go`, `.java`, `.c`, `.cpp`, `.rs`, `.zig`, `.sh`, `.sql` |
| Documents | `.pdf`, `.docx`, `.xlsx` |
| Data | `.json`, `.yaml`, `.yml`, `.xml`, `.html`, `.css`, `.toml` |

## MCP Tools Available

| Tool | Description |
|------|-------------|
| `search_documents` | Semantic search across indexed documents |
| `list_collections` | List all document collections |
| `list_documents` | List documents in a collection |
| `get_server_status` | Server status and stats |

## API Endpoints

### Backend (port 8900)
- `POST /api/documents/upload` - Upload and index a document
- `POST /api/documents/query` - Semantic search
- `GET /api/documents/list?collection=default` - List documents
- `DELETE /api/documents/{filename}` - Remove a document
- `POST /api/documents/reindex` - Re-index a collection
- `POST /api/smb/browse` - Browse SMB share
- `POST /api/smb/ingest` - Ingest from SMB share
- `GET /api/admin/status` - Server status

### MCP Server (port 8901)
- `GET /sse` - SSE transport endpoint
- `POST /messages?session_id=X` - SSE message endpoint
- `POST /mcp` - Streamable HTTP endpoint
- `GET /mcp/info` - Server capabilities (public)

## Data Storage

All persistent data is stored in `./data/`:
- `documents/` - Uploaded document files
- `chromadb/` - Vector database
- `config/` - Server configuration and API key hashes

## Management

```bash
# Start
docker compose up -d

# Stop
./stop.sh

# View logs
docker compose logs -f

# Rebuild after changes
docker compose build && docker compose up -d
```
7 changes: 7 additions & 0 deletions rag-mcp-server/backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__/
*.pyc
*.pyo
.git
.env
.venv
data/
30 changes: 30 additions & 0 deletions rag-mcp-server/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM python:3.12-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir torch==2.5.1 --index-url https://download.pytorch.org/whl/cpu \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get purge -y --auto-remove build-essential

# Pre-download the embedding model
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"

COPY . .

# Non-root runtime user. Data dirs are chowned on container start via entrypoint.
RUN useradd -m -u 10001 -s /usr/sbin/nologin appuser \
&& mkdir -p /app/data \
&& chown -R appuser:appuser /app \
&& cp -r /root/.cache /home/appuser/.cache \
&& chown -R appuser:appuser /home/appuser/.cache

USER appuser

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Empty file.
44 changes: 44 additions & 0 deletions rag-mcp-server/backend/app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import json
import os
from pathlib import Path

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
chroma_persist_dir: str = "/app/data/chromadb"
documents_dir: str = "/app/data/documents"
config_dir: str = "/app/data/config"
embedding_model: str = "all-MiniLM-L6-v2"
server_hostname: str = "BrownserverN5"
server_ip: str = "192.168.1.52"

class Config:
env_file = ".env"


settings = Settings()

CONFIG_FILE = Path(settings.config_dir) / "server_config.json"


def _ensure_config():
Path(settings.config_dir).mkdir(parents=True, exist_ok=True)
if not CONFIG_FILE.exists():
default = {
"api_keys": [],
"smb_shares": [],
"collections": ["default"],
"mcp_enabled": True,
}
CONFIG_FILE.write_text(json.dumps(default, indent=2))


def load_config() -> dict:
_ensure_config()
return json.loads(CONFIG_FILE.read_text())


def save_config(config: dict):
_ensure_config()
CONFIG_FILE.write_text(json.dumps(config, indent=2))
72 changes: 72 additions & 0 deletions rag-mcp-server/backend/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import logging
import os

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
from starlette.middleware.base import BaseHTTPMiddleware

from app.routers import admin, documents, smb

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s")
logger = logging.getLogger(__name__)


def _parse_origins(raw: str) -> list[str]:
origins = [o.strip() for o in raw.split(",") if o.strip()]
return origins or ["http://localhost", "http://127.0.0.1"]


CORS_ORIGINS = _parse_origins(os.environ.get("CORS_ALLOWED_ORIGINS", "http://192.168.1.52:8902,http://localhost:8902"))

limiter = Limiter(key_func=get_remote_address, default_limits=["120/minute"])

app = FastAPI(
title="RAG MCP Server - Backend API",
description="Document RAG engine with MCP server",
version="1.0.0",
)

app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type"],
max_age=600,
)


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "no-referrer"
response.headers["Cache-Control"] = "no-store"
return response


app.add_middleware(SecurityHeadersMiddleware)


@app.exception_handler(Exception)
async def _unhandled(request: Request, exc: Exception):
logger.exception("Unhandled error on %s %s", request.method, request.url.path)
return JSONResponse(status_code=500, content={"detail": "Internal server error"})


app.include_router(documents.router)
app.include_router(smb.router)
app.include_router(admin.router)


@app.get("/health")
async def health():
return {"status": "ok", "service": "rag-backend"}
Empty file.
Loading