Skip to content
Draft
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
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/src /app/src

EXPOSE 9001

CMD ["python", "src/server.py", "--host", "0.0.0.0", "--transport", "sse"]
# TODO Move to a path
ENTRYPOINT ["python", "src/server.py", "--host", "0.0.0.0", "--port", "9001", "--transport", "http", "--path", "/db-tools"]
9 changes: 9 additions & 0 deletions build/env.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
DB_HOST=yourdb
DB_USER=claude-mariadb
DB_PASSWORD=testing-testing-testing
DB_PORT=3306
DB_NAME=ThatDB
MCP_READ_ONLY=true
MCP_MAX_POOL_SIZE=10
JWT_ISSUER=https://truth.domain.tld
JWT_AUDIENCE=https://truth.domain.tld
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"asyncmy>=0.2.10",
"fastmcp[websockets]==2.12.1",
"fastmcp==2.13.1",
"google-genai>=1.15.0",
"openai>=1.78.1",
"python-dotenv>=1.1.0",
"sentence-transformers>=4.1.0",
"tokenizers==0.21.2",
"python-json-logger>=4.0.0"
]
51 changes: 19 additions & 32 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
# config.py
import os
from dotenv import load_dotenv
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path

# Import our dedicated logging configuration
from logging_config import setup_logger, get_logger, setup_third_party_logging

# Load environment variables from .env file
load_dotenv()

# --- Authentication Configuration ---
JWT_AUDIENCE = os.getenv("JWT_AUDIENCE", "mariadb_ops_server")
JWT_ISSUER = os.getenv("JWT_ISSUER", "http://localhost")

# --- Logging Configuration ---
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
LOG_FILE_PATH = os.getenv("LOG_FILE", "logs/mcp_server.log")
LOG_MAX_BYTES = int(os.getenv("LOG_MAX_BYTES", 10 * 1024 * 1024))
LOG_BACKUP_COUNT = int(os.getenv("LOG_BACKUP_COUNT", 5))
THIRD_PARTY_LOG_LEVEL = os.getenv("THIRD_PARTY_LOG_LEVEL", "WARNING").upper()

ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS")
if ALLOWED_ORIGINS:
Expand All @@ -26,36 +31,18 @@
else:
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]

# Get the root logger
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))

# Create formatter
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# Remove existing handlers to avoid duplication if script is reloaded
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)

# Console Handler
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
root_logger.addHandler(console_handler)

# File Handler - Ensure log directory exists
log_file = Path(LOG_FILE_PATH)
log_file.parent.mkdir(parents=True, exist_ok=True)

file_handler = RotatingFileHandler(
log_file,
maxBytes=LOG_MAX_BYTES,
backupCount=LOG_BACKUP_COUNT
# Set up the dedicated logger for this project (NOT the root logger)
logger = setup_logger(
log_level=LOG_LEVEL,
log_file_path=LOG_FILE_PATH,
log_max_bytes=LOG_MAX_BYTES,
log_backup_count=LOG_BACKUP_COUNT,
enable_console=True,
enable_file=True
)
file_handler.setFormatter(log_formatter)
root_logger.addHandler(file_handler)

# The specific logger used in server.py and elsewhere will inherit this configuration.
logger = logging.getLogger(__name__)
# Configure third-party library logging to reduce noise
setup_third_party_logging(level=THIRD_PARTY_LOG_LEVEL)

# --- Database Configuration ---
DB_HOST = os.getenv("DB_HOST", "localhost")
Expand Down Expand Up @@ -104,4 +91,4 @@
logger.info(f"No EMBEDDING_PROVIDER selected or it is set to None. Disabling embedding features.")

logger.info(f"Read-only mode: {MCP_READ_ONLY}")
logger.info(f"Logging to console and to file: {LOG_FILE_PATH} (Level: {LOG_LEVEL}, MaxSize: {LOG_MAX_BYTES}B, Backups: {LOG_BACKUP_COUNT})")
logger.info(f"Logging to console and to file: {LOG_FILE_PATH} (Level: {LOG_LEVEL}, MaxSize: {LOG_MAX_BYTES}B, Backups: {LOG_BACKUP_COUNT})")
11 changes: 7 additions & 4 deletions src/embeddings.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import logging
import sys
import os
import asyncio
from typing import List, Optional, Dict, Any, Union, Awaitable
import numpy as np

# Import configuration variables and the logger instance
# Import configuration variables
from config import (
EMBEDDING_PROVIDER,
OPENAI_API_KEY,
GEMINI_API_KEY,
HF_MODEL,
logger
HF_MODEL
)

# Import the dedicated logger
from logging_config import get_logger

logger = get_logger("embeddings")

# Import specific client libraries
try:
from openai import AsyncOpenAI, OpenAIError
Expand Down
153 changes: 153 additions & 0 deletions src/logging_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""
Logging configuration for the mariadb-mcp project.

This module sets up a dedicated logger that does NOT configure the root logger,
following Python best practices for library code.
"""
import logging
import os
from logging.handlers import RotatingFileHandler
from pathlib import Path
from pythonjsonlogger import jsonlogger


# Logger name for this project - NOT the root logger
LOGGER_NAME = "mariadb_mcp"


class CustomJsonFormatter(jsonlogger.JsonFormatter):
"""
Custom JSON formatter that includes timestamp, calling context,
and all relevant fields for structured logging.
"""
def add_fields(self, log_record, record, message_dict):
super(CustomJsonFormatter, self).add_fields(log_record, record, message_dict)

# Ensure timestamp is always present
if not log_record.get('timestamp'):
log_record['timestamp'] = self.formatTime(record, self.datefmt)

# Add calling context
log_record['level'] = record.levelname
log_record['logger'] = record.name
log_record['module'] = record.module
log_record['function'] = record.funcName
log_record['line'] = record.lineno

# Add process/thread info if relevant
if record.process:
log_record['process'] = record.process
if record.thread:
log_record['thread'] = record.thread


def setup_logger(
log_level: str = "INFO",
log_file_path: str = "logs/mcp_server.log",
log_max_bytes: int = 10 * 1024 * 1024,
log_backup_count: int = 5,
enable_console: bool = True,
enable_file: bool = True
) -> logging.Logger:
"""
Set up the dedicated logger for mariadb-mcp.

This function creates a logger with the name "mariadb_mcp" and configures
it with console and/or file handlers. It does NOT touch the root logger.

Args:
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
log_file_path: Path to the log file
log_max_bytes: Maximum size of log file before rotation
log_backup_count: Number of backup log files to keep
enable_console: Whether to enable console logging
enable_file: Whether to enable file logging

Returns:
Configured logger instance
"""
# Get the dedicated logger (NOT root logger)
logger = logging.getLogger(LOGGER_NAME)

# Set the level
logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))

# Prevent propagation to root logger to avoid duplicate logs
logger.propagate = False

# Remove any existing handlers to avoid duplication
for handler in logger.handlers[:]:
logger.removeHandler(handler)

# Create formatter with timestamp and calling context
formatter = CustomJsonFormatter(
fmt='%(timestamp)s %(level)s %(name)s %(module)s %(funcName)s:%(lineno)d %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)

# Console Handler
if enable_console:
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)

# File Handler
if enable_file:
# Ensure log directory exists
log_file = Path(log_file_path)
log_file.parent.mkdir(parents=True, exist_ok=True)

file_handler = RotatingFileHandler(
log_file,
maxBytes=log_max_bytes,
backupCount=log_backup_count
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

return logger


def get_logger(name: str = None) -> logging.Logger:
"""
Get a child logger under the mariadb_mcp logger hierarchy.

Args:
name: Optional name for the child logger. If None, returns the root mariadb_mcp logger.

Returns:
Logger instance
"""
if name:
return logging.getLogger(f"{LOGGER_NAME}.{name}")
return logging.getLogger(LOGGER_NAME)


def setup_third_party_logging(level: str = "WARNING"):
"""
Configure logging for third-party libraries like fastmcp, uvicorn, etc.

This sets the logging level for known third-party loggers to reduce noise
without touching the root logger.

Args:
level: Logging level for third-party libraries
"""
third_party_loggers = [
"fastmcp",
"uvicorn",
"uvicorn.access",
"uvicorn.error",
"starlette",
"asyncmy",
"httpx",
"httpcore"
]

log_level = getattr(logging, level.upper(), logging.WARNING)

for logger_name in third_party_loggers:
third_party_logger = logging.getLogger(logger_name)
third_party_logger.setLevel(log_level)
# Ensure they don't propagate excessively
third_party_logger.propagate = True
Loading