This is an old revision of the document!
Here is the converted DokuWiki-based markdown for the LibreChat setup tutorial.
```markdown
Addressed to: Libre
Domain: chat.haacksnetworking.org
Stack: Debian 13, npm (no Docker for LibreChat), Apache reverse proxy, PM2, MongoDB 8.0, DeepInfra DeepSeek V4 Flash, custom MCP document tools (docx, xlsx, pdf, pptx).
sudo apt update && sudo apt upgrade -y sudo apt install -y git build-essential curl gnupg python3-pip python3-venv
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - sudo apt install -y nodejs sudo npm install -g npm@11 node -v && npm -v
wget -qO - https://www.mongodb.org/static/pgp/server-8.0.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list sudo apt update sudo apt install -y mongodb-org sudo systemctl enable --now mongod
sudo npm install -g pm2
cd /opt sudo git clone https://github.com/LibreChat-AI/LibreChat.git sudo chown -R $USER:$USER LibreChat cd LibreChat cp .env.example .env nano .env
Essential .env values:
MONGO_URI=mongodb://127.0.0.1:27017/LibreChat JWT_SECRET=your_64_hex_secret JWT_REFRESH_SECRET=your_64_hex_secret CREDS_KEY=your_64_hex_secret CREDS_IV=your_32_hex_iv DOMAIN_CLIENT=https://chat.haacksnetworking.org DOMAIN_SERVER=https://chat.haacksnetworking.org TRUST_PROXY=1 HOST=127.0.0.1 DEEPINFRA_API_KEY=sk-di-your_key_here
Generate secrets with openssl rand -hex 32 (and openssl rand -hex 16 for the IV).
npm run reinstall npm run backend # test once, then Ctrl+C pm2 start npm --name librechat -- run backend pm2 save pm2 startup # follow the printed command
sudo a2enmod proxy proxy_http proxy_wstunnel ssl rewrite headers socache_shmcb sudo systemctl restart apache2
sudo apt install -y certbot python3-certbot-apache sudo certbot --apache -d chat.haacksnetworking.org
<VirtualHost *:443> ServerName chat.haacksnetworking.org SSLEngine on SSLCertificateFile /etc/letsencrypt/live/chat.haacksnetworking.org/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/chat.haacksnetworking.org/privkey.pem Include /etc/letsencrypt/options-ssl-apache.conf ProxyPreserveHost On ProxyRequests Off # Exclude downloads from proxy (must come before ProxyPass /) ProxyPass /downloads ! Alias /downloads /opt/LibreChat/generated_docs <Directory /opt/LibreChat/generated_docs> Options -Indexes Require all granted </Directory> ProxyPass / http://127.0.0.1:3080/ ProxyPassReverse / http://127.0.0.1:3080/ RequestHeader set X-Forwarded-Proto "https" RequestHeader set X-Forwarded-Port "443" RequestHeader set X-Real-IP "%{REMOTE_ADDR}s" RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s" RewriteEngine On RewriteCond %{HTTP:Upgrade} =websocket [NC] RewriteCond %{HTTP:Connection} upgrade [NC] RewriteRule ^/?(.*) "ws://127.0.0.1:3080/$1" [P,L] ProxyTimeout 300 Timeout 300 LimitRequestBody 26214400 ErrorLog ${APACHE_LOG_DIR}/librechat-error.log CustomLog ${APACHE_LOG_DIR}/librechat-access.log combined </VirtualHost> <VirtualHost *:80> ServerName chat.haacksnetworking.org Redirect permanent / https://chat.haacksnetworking.org/ </VirtualHost>
sudo a2ensite librechat sudo apache2ctl configtest sudo systemctl reload apache2
sudo ufw allow OpenSSH sudo ufw allow 'Apache Full' sudo ufw enable
Create or edit /opt/LibreChat/librechat.yaml:
version: 1.3.17 cache: true endpoints: custom: - name: "DeepInfra DeepSeek" apiKey: "${DEEPINFRA_API_KEY}" baseURL: "https://api.deepinfra.com/v1/openai" models: default: ["deepseek-ai/DeepSeek-V4-Flash"] fetch: false titleConvo: true titleModel: "deepseek-ai/DeepSeek-V4-Flash" modelDisplayLabel: "DeepSeek V4 Flash" webSearch: searchProvider: "keenable" scraperProvider: "keenable" rerankerType: "none" mcpServers: document-tools: type: stdio command: /opt/mcp-docs/venv/bin/python args: - /opt/mcp-docs/server.py timeout: 60000
cd /opt mkdir -p /opt/mcp-docs cd /opt/mcp-docs python3 -m venv venv source venv/bin/activate pip install mcp python-docx openpyxl pandas reportlab python-pptx
Create /opt/mcp-docs/server.py:
#!/usr/bin/env python3 import json import os from mcp.server import MCPServer from docx import Document from openpyxl import Workbook from openpyxl.styles import Font, PatternFill from reportlab.lib.pagesizes import letter from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer from reportlab.lib.styles import getSampleStyleSheet from pptx import Presentation OUTPUT_DIR = "/opt/LibreChat/generated_docs" os.makedirs(OUTPUT_DIR, exist_ok=True) mcp = MCPServer("document-tools") @mcp.tool() def create_docx(title: str, content: list = None, body: str = None) -> str: """Create a Word (.docx) document. Prefer content as a list of objects: [{"type":"heading|paragraph|bullet", "text":"..."}]. If that fails, pass body as a plain multi-line string. """ doc = Document() doc.add_heading(title, 0) items = content or [] if not items and body: for line in body.splitlines(): line = line.strip() if not line: continue if line.startswith("#"): doc.add_heading(line.lstrip("# ").strip(), level=1) elif line.startswith("- ") or line.startswith("* "): doc.add_paragraph(line[2:].strip(), style="List Bullet") else: doc.add_paragraph(line) else: for item in items: if isinstance(item, str): doc.add_paragraph(item) continue if not isinstance(item, dict): continue t = (item.get("type") or "paragraph").lower() text = item.get("text") or item.get("content") or "" if not text: continue if t == "heading": doc.add_heading(text, level=1) elif t == "bullet": doc.add_paragraph(text, style="List Bullet") else: doc.add_paragraph(text) filename = f"{title.replace(' ', '_')}.docx" path = os.path.join(OUTPUT_DIR, filename) doc.save(path) return json.dumps({ "success": True, "file": filename, "download": f"https://chat.haacksnetworking.org/downloads/{filename}" }) @mcp.tool() def create_xlsx(name: str, headers: list, rows: list) -> str: """Create an Excel (.xlsx) spreadsheet.""" wb = Workbook() ws = wb.active ws.title = name[:31] for col, h in enumerate(headers, 1): cell = ws.cell(1, col, h) cell.font = Font(bold=True, color="FFFFFF") cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") for r_idx, row in enumerate(rows, 2): for c_idx, val in enumerate(row, 1): ws.cell(r_idx, c_idx, val) filename = f"{name.replace(' ', '_')}.xlsx" path = os.path.join(OUTPUT_DIR, filename) wb.save(path) return json.dumps({ "success": True, "file": filename, "download": f"https://chat.haacksnetworking.org/downloads/{filename}" }) @mcp.tool() def create_pdf(title: str, content: list = None, body: str = None) -> str: """Create a PDF document. content is a list of dicts with type (heading/paragraph/bullet) and text, or pass body as plain text.""" filename = f"{title.replace(' ', '_')}.pdf" path = os.path.join(OUTPUT_DIR, filename) doc = SimpleDocTemplate(path, pagesize=letter) styles = getSampleStyleSheet() story = [] story.append(Paragraph(title, styles["Title"])) story.append(Spacer(1, 12)) items = content or [] if not items and body: for line in body.splitlines(): line = line.strip() if not line: continue if line.startswith("#"): story.append(Paragraph(line.lstrip("# ").strip(), styles["Heading2"])) elif line.startswith("- ") or line.startswith("* "): story.append(Paragraph(f"• {line[2:].strip()}", styles["Normal"])) else: story.append(Paragraph(line, styles["Normal"])) story.append(Spacer(1, 6)) else: for item in items: if isinstance(item, str): story.append(Paragraph(item, styles["Normal"])) story.append(Spacer(1, 6)) continue if not isinstance(item, dict): continue t = (item.get("type") or "paragraph").lower() text = item.get("text") or item.get("content") or "" if not text: continue if t == "heading": story.append(Paragraph(text, styles["Heading2"])) elif t == "bullet": story.append(Paragraph(f"• {text}", styles["Normal"])) else: story.append(Paragraph(text, styles["Normal"])) story.append(Spacer(1, 6)) doc.build(story) return json.dumps({ "success": True, "file": filename, "download": f"https://chat.haacksnetworking.org/downloads/{filename}" }) @mcp.tool() def create_pptx(title: str, slides: list = None) -> str: """Create a PowerPoint (.pptx). slides is a list of dicts with title and content (list of bullet strings).""" prs = Presentation() title_slide_layout = prs.slide_layouts[0] slide = prs.slides.add_slide(title_slide_layout) slide.shapes.title.text = title if len(slide.placeholders) > 1: try: slide.placeholders[1].text = "Generated by LibreChat" except Exception: pass bullet_layout = prs.slide_layouts[1] for s in (slides or []): slide = prs.slides.add_slide(bullet_layout) slide.shapes.title.text = s.get("title", "Slide") body = slide.placeholders[1] tf = body.text_frame tf.text = "" for bullet in s.get("content", []): p = tf.add_paragraph() p.text = str(bullet) p.level = 0 filename = f"{title.replace(' ', '_')}.pptx" path = os.path.join(OUTPUT_DIR, filename) prs.save(path) return json.dumps({ "success": True, "file": filename, "download": f"https://chat.haacksnetworking.org/downloads/{filename}" }) if __name__ == "__main__": mcp.run(transport="stdio")
chmod +x /opt/mcp-docs/server.py mkdir -p /opt/LibreChat/generated_docs deactivate
Restart LibreChat:
pm2 restart librechat
create_docx — Wordcreate_xlsx — Excelcreate_pdf — PDFcreate_pptx — PowerPointExample prompts:
Files appear at https://chat.haacksnetworking.org/downloads/….
First account is admin. Verify:
mongosh --quiet --eval 'db = db.getSiblingDB("LibreChat"); db.users.find({}, {email:1, role:1}).pretty()'
To disable public registration, add to .env:
ALLOW_REGISTRATION=false
Then create users with npm run create-user.
cd /opt/LibreChat pm2 stop librechat git pull npm run smart-reinstall pm2 restart librechat
1. Node 24 + MongoDB (Bookworm repo) 2. Clone → ''.env'' (secrets, domains, HOST, DeepInfra key) → ''npm run reinstall'' 3. PM2 4. Apache modules + Certbot + VirtualHost (with ''ProxyPass /downloads !'' + Alias) 5. ''librechat.yaml'' (DeepSeek + webSearch + mcpServers) 6. MCP server in ''/opt/mcp-docs'' with docx / xlsx / pdf / pptx tools 7. Restart LibreChat and enable the document-tools MCP server in chat
This is the full working configuration. ```