diff --git a/README.md b/README.md index ee4a3cc..4f5490b 100644 --- a/README.md +++ b/README.md @@ -130,24 +130,30 @@ A vector store table has the following columns: All configuration is via environment variables (typically set in a `.env` file): -| Variable | Description | Required | Default | -|------------------------|--------------------------------------------------------|----------|--------------| -| `DB_HOST` | MariaDB host address | Yes | `localhost` | -| `DB_PORT` | MariaDB port | No | `3306` | -| `DB_USER` | MariaDB username | Yes | | -| `DB_PASSWORD` | MariaDB password | Yes | | -| `DB_NAME` | Default database (optional; can be set per query) | No | | +| Variable | Description | Required | Default | +| ------------------------ | ---------------------------------------------------- | --------------------------------------- | ---------------- | +| `DB_HOST` | MariaDB host address | Yes | `localhost` | +| `DB_PORT` | MariaDB port | No | `3306` | +| `DB_USER` | MariaDB username | Yes | | +| `DB_PASSWORD` | MariaDB password | Yes | | +| `DB_NAME` | Default database (optional; can be set per query) | No | | | `DB_CHARSET` | Character set for database connection (e.g., `cp1251`) | No | MariaDB default | -| `MCP_READ_ONLY` | Enforce read-only SQL mode (`true`/`false`) | No | `true` | -| `MCP_MAX_POOL_SIZE` | Max DB connection pool size | No | `10` | -| `EMBEDDING_PROVIDER` | Embedding provider (`openai`/`gemini`/`huggingface`) | No |`None`(Disabled)| -| `OPENAI_API_KEY` | API key for OpenAI embeddings | Yes (if EMBEDDING_PROVIDER=openai) | | -| `GEMINI_API_KEY` | API key for Gemini embeddings | Yes (if EMBEDDING_PROVIDER=gemini) | | -| `HF_MODEL` | Open models from Huggingface | Yes (if EMBEDDING_PROVIDER=huggingface) | | +| `DB_SSL` | Enable SSL/TLS connections (`true`/`false`) | No | `false` | +| `DB_SSL_CA` | Path to SSL certificate authority file | No | | +| `DB_SSL_CERT` | Path to SSL client certificate file | No | | +| `DB_SSL_KEY` | Path to SSL client private key file | No | | +| `DB_SSL_VERIFY_CERT` | Verify SSL certificate (`true`/`false`) | No | `true` | +| `DB_SSL_VERIFY_IDENTITY` | Verify SSL server identity (`true`/`false`) | No | `false` | +| `MCP_READ_ONLY` | Enforce read-only SQL mode (`true`/`false`) | No | `true` | +| `MCP_MAX_POOL_SIZE` | Max DB connection pool size | No | `10` | +| `EMBEDDING_PROVIDER` | Embedding provider (`openai`/`gemini`/`huggingface`) | No | `None`(Disabled) | +| `OPENAI_API_KEY` | API key for OpenAI embeddings | Yes (if EMBEDDING_PROVIDER=openai) | | +| `GEMINI_API_KEY` | API key for Gemini embeddings | Yes (if EMBEDDING_PROVIDER=gemini) | | +| `HF_MODEL` | Open models from Huggingface | Yes (if EMBEDDING_PROVIDER=huggingface) | | #### Example `.env` file -**With Embedding Support (OpenAI):** +**With Embedding Support (OpenAI) and SSL:** ```dotenv DB_HOST=localhost DB_USER=your_db_user @@ -155,6 +161,14 @@ DB_PASSWORD=your_db_password DB_PORT=3306 DB_NAME=your_default_database +# SSL Configuration +DB_SSL=true +DB_SSL_CA=/path/to/ca-cert.pem +DB_SSL_CERT=/path/to/client-cert.pem +DB_SSL_KEY=/path/to/client-key.pem +DB_SSL_VERIFY_CERT=true +DB_SSL_VERIFY_IDENTITY=false + MCP_READ_ONLY=true MCP_MAX_POOL_SIZE=10 @@ -175,6 +189,14 @@ MCP_READ_ONLY=true MCP_MAX_POOL_SIZE=10 ``` +**SSL Configuration Notes:** + +- Set `DB_SSL=true` to enable SSL/TLS connections +- Certificate paths should be absolute paths to the certificate files +- Three levels of SSL verification: + 1. **Basic SSL** (`DB_SSL=true`, no certificates): Encrypts connection without certificate verification + 2. **Certificate verification** (`DB_SSL_VERIFY_CERT=true`): Verifies server certificate against CA + 3. **Full verification** (`DB_SSL_VERIFY_CERT=true`, `DB_SSL_VERIFY_IDENTITY=true`): Verifies certificate and server identity --- ## Installation & Setup diff --git a/src/config.py b/src/config.py index cb35d65..26711ae 100644 --- a/src/config.py +++ b/src/config.py @@ -53,6 +53,14 @@ DB_NAME = os.getenv("DB_NAME") DB_CHARSET = os.getenv("DB_CHARSET") +# --- SSL Configuration --- +DB_SSL = os.getenv("DB_SSL", "false").lower() == "true" +DB_SSL_CA = os.getenv("DB_SSL_CA") # Path to CA certificate +DB_SSL_CERT = os.getenv("DB_SSL_CERT") # Path to client certificate +DB_SSL_KEY = os.getenv("DB_SSL_KEY") # Path to client private key +DB_SSL_VERIFY_CERT = os.getenv("DB_SSL_VERIFY_CERT", "true").lower() == "true" +DB_SSL_VERIFY_IDENTITY = os.getenv("DB_SSL_VERIFY_IDENTITY", "false").lower() == "true" + # --- MCP Server Configuration --- # Read-only mode MCP_READ_ONLY = os.getenv("MCP_READ_ONLY", "true").lower() == "true" diff --git a/src/server.py b/src/server.py index 2ff5afc..34d8566 100644 --- a/src/server.py +++ b/src/server.py @@ -5,6 +5,8 @@ import re from typing import List, Dict, Any, Optional from functools import partial +import os +import ssl import asyncmy import anyio @@ -13,6 +15,7 @@ # Import configuration settings from config import ( DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, DB_CHARSET, + DB_SSL, DB_SSL_CA, DB_SSL_CERT, DB_SSL_KEY, DB_SSL_VERIFY_CERT, DB_SSL_VERIFY_IDENTITY, MCP_READ_ONLY, MCP_MAX_POOL_SIZE, EMBEDDING_PROVIDER, logger ) @@ -72,6 +75,43 @@ async def initialize_pool(self): return try: + if DB_CHARSET: + pool_params["charset"] = DB_CHARSET + logger.info(f"Creating connection pool for {DB_USER}@{DB_HOST}:{DB_PORT}/{DB_NAME} (max size: {MCP_MAX_POOL_SIZE}, charset: {DB_CHARSET})") + else: + logger.info(f"Creating connection pool for {DB_USER}@{DB_HOST}:{DB_PORT}/{DB_NAME} (max size: {MCP_MAX_POOL_SIZE})") + + if DB_SSL: + ssl_context = ssl.create_default_context() + if DB_SSL_CA: + if os.path.exists(DB_SSL_CA): + ssl_context.load_verify_locations(cafile=DB_SSL_CA) + logger.info(f"Loaded SSL CA certificate: {DB_SSL_CA}") + else: + logger.warning(f"SSL CA certificate file not found: {DB_SSL_CA}") + + if DB_SSL_CERT and DB_SSL_KEY: + if os.path.exists(DB_SSL_CERT) and os.path.exists(DB_SSL_KEY): + ssl_context.load_cert_chain(DB_SSL_CERT, DB_SSL_KEY) + logger.info(f"Loaded SSL client certificate: {DB_SSL_CERT}") + else: + logger.warning(f"SSL client certificate files not found: cert={DB_SSL_CERT}, key={DB_SSL_KEY}") + + if not DB_SSL_VERIFY_CERT: + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + logger.info("SSL certificate verification disabled") + elif not DB_SSL_VERIFY_IDENTITY: + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_REQUIRED + logger.info("SSL hostname verification disabled, certificate verification enabled") + else: + logger.info("Full SSL verification enabled") + + logger.info("SSL enabled for database connection") + else: + logger.info("SSL disabled for database connection") + pool_params = { "host": DB_HOST, "port": DB_PORT, @@ -81,15 +121,10 @@ async def initialize_pool(self): "minsize": 1, "maxsize": MCP_MAX_POOL_SIZE, "autocommit": self.autocommit, - "pool_recycle": 3600 + "pool_recycle": 3600, + "ssl_context"=ssl_context, } - if DB_CHARSET: - pool_params["charset"] = DB_CHARSET - logger.info(f"Creating connection pool for {DB_USER}@{DB_HOST}:{DB_PORT}/{DB_NAME} (max size: {MCP_MAX_POOL_SIZE}, charset: {DB_CHARSET})") - else: - logger.info(f"Creating connection pool for {DB_USER}@{DB_HOST}:{DB_PORT}/{DB_NAME} (max size: {MCP_MAX_POOL_SIZE})") - self.pool = await asyncmy.create_pool(**pool_params) logger.info("Connection pool initialized successfully.") except AsyncMyError as e: