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
12 changes: 11 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@ SSO_INTROSPECTION_MODE=rfc7662
# SSO_INTROSPECTION_URL=https://oauth2.googleapis.com/tokeninfo
SSO_INTROSPECTION_URL=https://sso.example.com/realms/myrealm/protocol/openid-connect/token/introspect

# --- PostgreSQL (required only if ENABLE_AUTH=True, for token storage) ---
# --- Storage Provider (required only if ENABLE_AUTH=True) ---
# Valid values: postgres, redis
STORAGE_TYPE=postgres

# --- PostgreSQL (required if STORAGE_TYPE=postgres) ---
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=postgres_db
POSTGRES_USER=postgres_user
POSTGRES_PASSWORD=postgres_password

# --- Redis (required if STORAGE_TYPE=redis) ---
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
25 changes: 25 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ services:
networks:
- template-network

redis:
image: redis:7-alpine
container_name: template-mcp-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
networks:
- template-network

template-mcp-server:
build:
context: .
Expand All @@ -37,9 +54,15 @@ services:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- STORAGE_TYPE=postgres
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_DB=0
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: always
healthcheck:
test: [ "CMD", "curl", "-f", "-k", "http://0.0.0.0:5001/health"]
Expand All @@ -52,6 +75,8 @@ services:
volumes:
postgres_data:
driver: local
redis_data:
driver: local

networks:
template-network:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies = [
"psycopg==3.2.3",
"itsdangerous==2.2.0",
"requests-oauthlib==2.0.0",
"redis>=5.0.1",
]

[project.optional-dependencies]
Expand Down
104 changes: 59 additions & 45 deletions template_mcp_server/src/oauth/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
from typing import Any, Dict, List, Optional

from template_mcp_server.src.settings import settings
from template_mcp_server.src.storage.base import BaseStorageService
from template_mcp_server.src.storage.redis_service import RedisStorageService
from template_mcp_server.src.storage.storage_service import StorageService
from template_mcp_server.utils.pylogger import get_python_logger

logger = get_python_logger(settings.PYTHON_LOG_LEVEL)

# Global storage service for backward compatibility during transition
_storage_service: Optional[StorageService] = None
_storage_service: Optional[BaseStorageService] = None


def generate_random_string(length: int = 32) -> str:
Expand Down Expand Up @@ -66,7 +68,7 @@ def verify_code_challenge(code_verifier: str, code_challenge: str) -> bool:
class OAuthService:
"""OAuth service that manages OAuth 2.0 operations with dependency injection."""

def __init__(self, storage_service: StorageService):
def __init__(self, storage_service: BaseStorageService):
"""Initialize OAuth service with storage dependency.

Args:
Expand Down Expand Up @@ -239,13 +241,13 @@ async def get_storage_status(self) -> Dict[str, Any]:


# Backward compatibility functions - will be deprecated in future versions
async def get_storage_service() -> StorageService:
async def get_storage_service() -> BaseStorageService:
"""Get the initialized storage service.

Note: Storage service must be initialized via initialize_storage() during startup.

Returns:
StorageService: The initialized storage service
BaseStorageService: The initialized storage service

Raises:
RuntimeError: If storage service hasn't been initialized
Expand Down Expand Up @@ -380,62 +382,74 @@ async def get_storage_status() -> Dict[str, Any]:
return await service.get_storage_status()


async def initialize_storage() -> StorageService:
async def initialize_storage() -> BaseStorageService:
"""Initialize the storage service. Call this during application startup.

Returns:
StorageService: The initialized storage service
BaseStorageService: The initialized storage service

Raises:
ValueError: If PostgreSQL configuration is missing
ConnectionError: If PostgreSQL connection fails
ValueError: If configuration is missing
ConnectionError: If connection fails
"""
global _storage_service

if _storage_service is not None:
logger.warning("Storage service already initialized")
return _storage_service

logger.info("Initializing PostgreSQL storage service")

# Validate required configuration
if not all(
[
settings.POSTGRES_HOST,
settings.POSTGRES_PORT,
settings.POSTGRES_DB,
settings.POSTGRES_USER,
]
):
missing = [
name
for name, value in [
("POSTGRES_HOST", settings.POSTGRES_HOST),
("POSTGRES_PORT", settings.POSTGRES_PORT),
("POSTGRES_DB", settings.POSTGRES_DB),
("POSTGRES_USER", settings.POSTGRES_USER),
if settings.STORAGE_TYPE == "redis":
logger.info("Initializing Redis storage service")
_storage_service = RedisStorageService(
host=settings.REDIS_HOST or "localhost",
port=settings.REDIS_PORT,
password=settings.REDIS_PASSWORD,
db=settings.REDIS_DB,
)
else:
logger.info("Initializing PostgreSQL storage service")

# Validate required configuration
if not all(
[
settings.POSTGRES_HOST,
settings.POSTGRES_PORT,
settings.POSTGRES_DB,
settings.POSTGRES_USER,
]
):
missing = [
name
for name, value in [
("POSTGRES_HOST", settings.POSTGRES_HOST),
("POSTGRES_PORT", settings.POSTGRES_PORT),
("POSTGRES_DB", settings.POSTGRES_DB),
("POSTGRES_USER", settings.POSTGRES_USER),
]
if not value
]
if not value
]
raise ValueError(
f"Missing required PostgreSQL configuration: {', '.join(missing)}"
raise ValueError(
f"Missing required PostgreSQL configuration: {', '.join(missing)}"
)

# Create and connect storage service
# Type assertions are safe here because we validated required fields above
_storage_service = StorageService(
host=str(settings.POSTGRES_HOST),
port=int(settings.POSTGRES_PORT)
if settings.POSTGRES_PORT is not None
else 5432,
database=str(settings.POSTGRES_DB),
username=str(settings.POSTGRES_USER),
password=settings.POSTGRES_PASSWORD or "",
pool_size=settings.POSTGRES_POOL_SIZE,
max_connections=settings.POSTGRES_MAX_CONNECTIONS,
)

# Create and connect storage service
# Type assertions are safe here because we validated required fields above
_storage_service = StorageService(
host=str(settings.POSTGRES_HOST),
port=int(settings.POSTGRES_PORT)
if settings.POSTGRES_PORT is not None
else 5432,
database=str(settings.POSTGRES_DB),
username=str(settings.POSTGRES_USER),
password=settings.POSTGRES_PASSWORD or "",
pool_size=settings.POSTGRES_POOL_SIZE,
max_connections=settings.POSTGRES_MAX_CONNECTIONS,
)
await _storage_service.connect()
logger.info("PostgreSQL storage service initialized successfully")
logger.info(
f"{settings.STORAGE_TYPE.capitalize()} storage service initialized successfully"
)

return _storage_service

Expand All @@ -444,7 +458,7 @@ async def cleanup_storage() -> None:
"""Cleanup storage service. Call this during application shutdown."""
global _storage_service
if _storage_service is not None:
logger.info("Disconnecting from PostgreSQL...")
logger.info("Disconnecting from storage...")
await _storage_service.disconnect()
_storage_service = None
logger.info("Storage service cleanup complete")
48 changes: 48 additions & 0 deletions template_mcp_server/src/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,54 @@ class Settings(BaseSettings):
},
)

# Storage Configuration
STORAGE_TYPE: Literal["postgres", "redis"] = Field(
default="postgres",
json_schema_extra={
"env": "STORAGE_TYPE",
"description": "Storage backend type: postgres or redis",
"example": "postgres",
"enum": ["postgres", "redis"],
},
)

# Redis Configuration
REDIS_HOST: Optional[str] = Field(
default="localhost",
json_schema_extra={
"env": "REDIS_HOST",
"description": "Redis host address",
"example": "localhost",
},
)
REDIS_PORT: int = Field(
default=6379,
ge=1024,
le=65535,
json_schema_extra={
"env": "REDIS_PORT",
"description": "Redis port number",
"example": 6379,
},
)
REDIS_PASSWORD: Optional[str] = Field(
default=None,
json_schema_extra={
"env": "REDIS_PASSWORD",
"description": "Redis password",
"example": "secretpassword",
"sensitive": True,
},
)
REDIS_DB: int = Field(
default=0,
json_schema_extra={
"env": "REDIS_DB",
"description": "Redis database index",
"example": 0,
},
)

# PostgreSQL Configuration
POSTGRES_HOST: Optional[str] = Field(
default=None,
Expand Down
8 changes: 7 additions & 1 deletion template_mcp_server/src/storage/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
"""PostgreSQL storage service for the Template MCP Server."""
"""Storage services for the Template MCP Server."""

from template_mcp_server.src.storage.base import BaseStorageService
from template_mcp_server.src.storage.redis_service import RedisStorageService
from template_mcp_server.src.storage.storage_service import StorageService

__all__ = ["BaseStorageService", "StorageService", "RedisStorageService"]
Loading