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/
19 changes: 19 additions & 0 deletions rag-mcp-server/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 -r requirements.txt

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

COPY . .

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))
31 changes: 31 additions & 0 deletions rag-mcp-server/backend/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.routers import admin, documents, smb

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

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

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

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.
94 changes: 94 additions & 0 deletions rag-mcp-server/backend/app/models/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from pydantic import BaseModel


class DocumentUpload(BaseModel):
collection: str = "default"


class QueryRequest(BaseModel):
query: str
collection: str = "default"
n_results: int = 5


class QueryResult(BaseModel):
content: str
source: str
score: float
metadata: dict


class QueryResponse(BaseModel):
results: list[QueryResult]
query: str


class CollectionInfo(BaseModel):
name: str
document_count: int


class SMBShareConfig(BaseModel):
name: str
server: str
share: str
username: str = "guest"
password: str = ""
domain: str = "WORKGROUP"
path: str = "/"


class SMBListSharesRequest(BaseModel):
server: str
username: str = "guest"
password: str = ""
domain: str = "WORKGROUP"


class SMBBrowseRequest(BaseModel):
server: str
share: str
path: str = "/"
username: str = "guest"
password: str = ""
domain: str = "WORKGROUP"


class SMBFileEntry(BaseModel):
name: str
is_directory: bool
size: int
last_modified: str


class APIKeyCreate(BaseModel):
name: str
description: str = ""


class APIKeyResponse(BaseModel):
name: str
key_prefix: str
description: str
created_at: str
active: bool


class IngestSMBRequest(BaseModel):
server: str
share: str
path: str
username: str = "guest"
password: str = ""
domain: str = "WORKGROUP"
collection: str = "default"
recursive: bool = True


class ServerStatus(BaseModel):
hostname: str
ip: str
mcp_enabled: bool
total_documents: int
collections: list[str]
api_keys_count: int
Empty file.
Loading