Files
superset2/superset/mcp_service/storage.py

144 lines
4.3 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
MCP Redis storage factory.
Provides get_mcp_store(prefix) factory for creating stores with feature-specific
prefixes. Uses shared MCP_STORE_CONFIG for Redis URL and wrapper type.
Reusable across caching middleware, OAuth providers, EventStore, etc.
"""
import logging
from importlib import import_module
from typing import Any, Callable, Dict
logger = logging.getLogger(__name__)
def get_mcp_store(
prefix: str | Callable[[], str],
) -> Any | None:
"""
Create a store instance with the specified prefix.
Uses shared MCP_STORE_CONFIG for Redis URL and wrapper type.
Each caller provides their own prefix (cache, auth, events, etc.).
Args:
prefix: Feature-specific prefix (string or callable for multi-tenancy)
Returns:
Wrapped RedisStore instance or None if not configured/disabled
Examples:
# Caching
cache_store = get_mcp_store(prefix=cache_prefix_lambda)
# Auth (future)
auth_store = get_mcp_store(prefix="mcp_auth_v1_")
# EventStore (future)
event_store = get_mcp_store(prefix=event_prefix_lambda)
"""
from flask import has_app_context
from superset.mcp_service.flask_singleton import get_flask_app
flask_app = get_flask_app()
def _get_store() -> Any | None:
store_config = flask_app.config.get("MCP_STORE_CONFIG", {})
# Check if store is enabled
if not store_config.get("enabled", False):
logger.debug("MCP store disabled via config")
return None
return _create_redis_store(store_config, prefix)
# Use existing app context if available, otherwise push one
if has_app_context():
return _get_store()
else:
with flask_app.app_context():
return _get_store()
def _create_redis_store(
store_config: Dict[str, Any],
prefix: str | Callable[[], str],
) -> Any | None:
"""
Create a RedisStore with the given prefix.
Args:
store_config: MCP_STORE_CONFIG dict (Redis URL, wrapper type)
prefix: Feature-specific prefix
Returns:
Wrapped RedisStore instance or None if not configured
"""
redis_url = store_config.get("CACHE_REDIS_URL")
if not redis_url:
logger.debug("MCP storage disabled - no CACHE_REDIS_URL configured")
return None
try:
from key_value.aio.stores.redis import RedisStore
except ImportError:
logger.warning(
"key_value package not available for Redis storage. "
"Install with: pip install py-key-value-aio[redis]"
)
return None
try:
wrapper_type = store_config.get("WRAPPER_TYPE")
if not wrapper_type:
logger.error("MCP store WRAPPER_TYPE not configured")
return None
wrapper_class = _import_wrapper_class(wrapper_type)
redis_store = RedisStore(url=redis_url)
store = wrapper_class(key_value=redis_store, prefix=prefix)
logger.info("✅ MCP RedisStore created")
return store
except Exception as e:
logger.error("Failed to create MCP store: %s", e)
return None
def _import_wrapper_class(class_path: str) -> type:
"""
Import a wrapper class from a dotted path.
Args:
class_path: Dotted path like
'key_value.aio.wrappers.prefix_keys.PrefixKeysWrapper'
Returns:
The imported class
Raises:
ImportError: If the class cannot be imported
"""
module_path, class_name = class_path.rsplit(".", 1)
module = import_module(module_path)
return getattr(module, class_name)