kailash 0.2.2__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +1 -1
- kailash/access_control.py +40 -39
- kailash/api/auth.py +26 -32
- kailash/api/custom_nodes.py +29 -29
- kailash/api/custom_nodes_secure.py +35 -35
- kailash/api/database.py +17 -17
- kailash/api/gateway.py +19 -19
- kailash/api/mcp_integration.py +24 -23
- kailash/api/studio.py +45 -45
- kailash/api/workflow_api.py +8 -8
- kailash/cli/commands.py +5 -8
- kailash/manifest.py +42 -42
- kailash/mcp/__init__.py +1 -1
- kailash/mcp/ai_registry_server.py +20 -20
- kailash/mcp/client.py +9 -11
- kailash/mcp/client_new.py +10 -10
- kailash/mcp/server.py +1 -2
- kailash/mcp/server_enhanced.py +449 -0
- kailash/mcp/servers/ai_registry.py +6 -6
- kailash/mcp/utils/__init__.py +31 -0
- kailash/mcp/utils/cache.py +267 -0
- kailash/mcp/utils/config.py +263 -0
- kailash/mcp/utils/formatters.py +293 -0
- kailash/mcp/utils/metrics.py +418 -0
- kailash/nodes/ai/agents.py +9 -9
- kailash/nodes/ai/ai_providers.py +33 -34
- kailash/nodes/ai/embedding_generator.py +31 -32
- kailash/nodes/ai/intelligent_agent_orchestrator.py +62 -66
- kailash/nodes/ai/iterative_llm_agent.py +48 -48
- kailash/nodes/ai/llm_agent.py +32 -33
- kailash/nodes/ai/models.py +13 -13
- kailash/nodes/ai/self_organizing.py +44 -44
- kailash/nodes/api/__init__.py +5 -0
- kailash/nodes/api/auth.py +11 -11
- kailash/nodes/api/graphql.py +13 -13
- kailash/nodes/api/http.py +19 -19
- kailash/nodes/api/monitoring.py +463 -0
- kailash/nodes/api/rate_limiting.py +9 -13
- kailash/nodes/api/rest.py +29 -29
- kailash/nodes/api/security.py +819 -0
- kailash/nodes/base.py +24 -26
- kailash/nodes/base_async.py +7 -7
- kailash/nodes/base_cycle_aware.py +12 -12
- kailash/nodes/base_with_acl.py +5 -5
- kailash/nodes/code/python.py +56 -55
- kailash/nodes/data/__init__.py +6 -0
- kailash/nodes/data/directory.py +6 -6
- kailash/nodes/data/event_generation.py +297 -0
- kailash/nodes/data/file_discovery.py +598 -0
- kailash/nodes/data/readers.py +8 -8
- kailash/nodes/data/retrieval.py +10 -10
- kailash/nodes/data/sharepoint_graph.py +17 -17
- kailash/nodes/data/sources.py +5 -5
- kailash/nodes/data/sql.py +13 -13
- kailash/nodes/data/streaming.py +25 -25
- kailash/nodes/data/vector_db.py +22 -22
- kailash/nodes/data/writers.py +7 -7
- kailash/nodes/logic/async_operations.py +17 -17
- kailash/nodes/logic/convergence.py +11 -11
- kailash/nodes/logic/loop.py +4 -4
- kailash/nodes/logic/operations.py +11 -11
- kailash/nodes/logic/workflow.py +8 -9
- kailash/nodes/mixins/mcp.py +17 -17
- kailash/nodes/mixins.py +8 -10
- kailash/nodes/transform/chunkers.py +3 -3
- kailash/nodes/transform/formatters.py +7 -7
- kailash/nodes/transform/processors.py +11 -11
- kailash/runtime/access_controlled.py +18 -18
- kailash/runtime/async_local.py +18 -20
- kailash/runtime/docker.py +24 -26
- kailash/runtime/local.py +55 -31
- kailash/runtime/parallel.py +25 -25
- kailash/runtime/parallel_cyclic.py +29 -29
- kailash/runtime/runner.py +6 -6
- kailash/runtime/testing.py +22 -22
- kailash/sdk_exceptions.py +0 -58
- kailash/security.py +14 -26
- kailash/tracking/manager.py +38 -38
- kailash/tracking/metrics_collector.py +15 -14
- kailash/tracking/models.py +53 -53
- kailash/tracking/storage/base.py +7 -17
- kailash/tracking/storage/database.py +22 -23
- kailash/tracking/storage/filesystem.py +38 -40
- kailash/utils/export.py +21 -21
- kailash/utils/templates.py +8 -9
- kailash/visualization/api.py +30 -34
- kailash/visualization/dashboard.py +17 -17
- kailash/visualization/performance.py +32 -19
- kailash/visualization/reports.py +30 -28
- kailash/workflow/builder.py +8 -8
- kailash/workflow/convergence.py +13 -12
- kailash/workflow/cycle_analyzer.py +38 -33
- kailash/workflow/cycle_builder.py +12 -12
- kailash/workflow/cycle_config.py +16 -15
- kailash/workflow/cycle_debugger.py +40 -40
- kailash/workflow/cycle_exceptions.py +29 -29
- kailash/workflow/cycle_profiler.py +21 -21
- kailash/workflow/cycle_state.py +20 -22
- kailash/workflow/cyclic_runner.py +45 -45
- kailash/workflow/graph.py +57 -45
- kailash/workflow/mermaid_visualizer.py +9 -11
- kailash/workflow/migration.py +22 -22
- kailash/workflow/mock_registry.py +6 -6
- kailash/workflow/runner.py +9 -9
- kailash/workflow/safety.py +12 -13
- kailash/workflow/state.py +8 -11
- kailash/workflow/templates.py +19 -19
- kailash/workflow/validation.py +14 -14
- kailash/workflow/visualization.py +32 -24
- kailash-0.3.1.dist-info/METADATA +476 -0
- kailash-0.3.1.dist-info/RECORD +136 -0
- kailash-0.2.2.dist-info/METADATA +0 -121
- kailash-0.2.2.dist-info/RECORD +0 -126
- {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/WHEEL +0 -0
- {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,267 @@
|
|
1
|
+
"""
|
2
|
+
Caching utilities for MCP servers.
|
3
|
+
|
4
|
+
Provides LRU cache, TTL support, and decorators for method-level caching.
|
5
|
+
Based on patterns from production MCP server implementations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import functools
|
10
|
+
import logging
|
11
|
+
import threading
|
12
|
+
import time
|
13
|
+
from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
18
|
+
|
19
|
+
|
20
|
+
class LRUCache:
|
21
|
+
"""
|
22
|
+
Thread-safe LRU cache with TTL (time-to-live) support.
|
23
|
+
|
24
|
+
Features:
|
25
|
+
- Configurable maximum size
|
26
|
+
- TTL expiration for entries
|
27
|
+
- Thread-safe operations
|
28
|
+
- Performance statistics
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, max_size: int = 128, ttl: int = 300):
|
32
|
+
"""
|
33
|
+
Initialize LRU cache.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
max_size: Maximum number of entries to store
|
37
|
+
ttl: Time-to-live in seconds (0 = no expiration)
|
38
|
+
"""
|
39
|
+
self.max_size = max_size
|
40
|
+
self.ttl = ttl
|
41
|
+
self._cache: Dict[str, Tuple[Any, float]] = {}
|
42
|
+
self._access_order: Dict[str, float] = {}
|
43
|
+
self._lock = threading.RLock()
|
44
|
+
|
45
|
+
# Statistics
|
46
|
+
self._hits = 0
|
47
|
+
self._misses = 0
|
48
|
+
self._evictions = 0
|
49
|
+
|
50
|
+
def get(self, key: str) -> Optional[Any]:
|
51
|
+
"""Get value from cache if it exists and hasn't expired."""
|
52
|
+
with self._lock:
|
53
|
+
if key not in self._cache:
|
54
|
+
self._misses += 1
|
55
|
+
return None
|
56
|
+
|
57
|
+
value, timestamp = self._cache[key]
|
58
|
+
|
59
|
+
# Check TTL expiration
|
60
|
+
if self.ttl > 0 and time.time() - timestamp > self.ttl:
|
61
|
+
del self._cache[key]
|
62
|
+
del self._access_order[key]
|
63
|
+
self._misses += 1
|
64
|
+
return None
|
65
|
+
|
66
|
+
# Update access time for LRU
|
67
|
+
self._access_order[key] = time.time()
|
68
|
+
self._hits += 1
|
69
|
+
return value
|
70
|
+
|
71
|
+
def set(self, key: str, value: Any) -> None:
|
72
|
+
"""Set value in cache, evicting LRU items if necessary."""
|
73
|
+
with self._lock:
|
74
|
+
current_time = time.time()
|
75
|
+
|
76
|
+
# If key exists, update it
|
77
|
+
if key in self._cache:
|
78
|
+
self._cache[key] = (value, current_time)
|
79
|
+
self._access_order[key] = current_time
|
80
|
+
return
|
81
|
+
|
82
|
+
# Check if we need to evict
|
83
|
+
if len(self._cache) >= self.max_size:
|
84
|
+
self._evict_lru()
|
85
|
+
|
86
|
+
# Add new entry
|
87
|
+
self._cache[key] = (value, current_time)
|
88
|
+
self._access_order[key] = current_time
|
89
|
+
|
90
|
+
def _evict_lru(self) -> None:
|
91
|
+
"""Evict least recently used item."""
|
92
|
+
if not self._access_order:
|
93
|
+
return
|
94
|
+
|
95
|
+
lru_key = min(self._access_order.keys(), key=self._access_order.get)
|
96
|
+
del self._cache[lru_key]
|
97
|
+
del self._access_order[lru_key]
|
98
|
+
self._evictions += 1
|
99
|
+
|
100
|
+
def clear(self) -> None:
|
101
|
+
"""Clear all entries from cache."""
|
102
|
+
with self._lock:
|
103
|
+
self._cache.clear()
|
104
|
+
self._access_order.clear()
|
105
|
+
|
106
|
+
def stats(self) -> Dict[str, Any]:
|
107
|
+
"""Get cache performance statistics."""
|
108
|
+
with self._lock:
|
109
|
+
total_requests = self._hits + self._misses
|
110
|
+
hit_rate = self._hits / total_requests if total_requests > 0 else 0
|
111
|
+
|
112
|
+
return {
|
113
|
+
"hits": self._hits,
|
114
|
+
"misses": self._misses,
|
115
|
+
"evictions": self._evictions,
|
116
|
+
"hit_rate": hit_rate,
|
117
|
+
"size": len(self._cache),
|
118
|
+
"max_size": self.max_size,
|
119
|
+
"ttl": self.ttl,
|
120
|
+
}
|
121
|
+
|
122
|
+
|
123
|
+
class CacheManager:
|
124
|
+
"""
|
125
|
+
High-level cache management with multiple caching strategies.
|
126
|
+
|
127
|
+
Provides easy-to-use caching for MCP servers with different cache types
|
128
|
+
for different use cases.
|
129
|
+
"""
|
130
|
+
|
131
|
+
def __init__(self, enabled: bool = True, default_ttl: int = 300):
|
132
|
+
"""
|
133
|
+
Initialize cache manager.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
enabled: Whether caching is enabled
|
137
|
+
default_ttl: Default TTL for cache entries
|
138
|
+
"""
|
139
|
+
self.enabled = enabled
|
140
|
+
self.default_ttl = default_ttl
|
141
|
+
self._caches: Dict[str, LRUCache] = {}
|
142
|
+
|
143
|
+
def get_cache(
|
144
|
+
self, name: str, max_size: int = 128, ttl: Optional[int] = None
|
145
|
+
) -> LRUCache:
|
146
|
+
"""Get or create a named cache."""
|
147
|
+
if name not in self._caches:
|
148
|
+
cache_ttl = ttl if ttl is not None else self.default_ttl
|
149
|
+
self._caches[name] = LRUCache(max_size=max_size, ttl=cache_ttl)
|
150
|
+
return self._caches[name]
|
151
|
+
|
152
|
+
def cached(self, cache_name: str = "default", ttl: Optional[int] = None):
|
153
|
+
"""
|
154
|
+
Decorator to cache function results.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
cache_name: Name of cache to use
|
158
|
+
ttl: TTL for this specific cache
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
Decorated function with caching
|
162
|
+
"""
|
163
|
+
|
164
|
+
def decorator(func: F) -> F:
|
165
|
+
if not self.enabled:
|
166
|
+
return func
|
167
|
+
|
168
|
+
cache = self.get_cache(cache_name, ttl=ttl)
|
169
|
+
|
170
|
+
@functools.wraps(func)
|
171
|
+
def sync_wrapper(*args, **kwargs):
|
172
|
+
# Create cache key from function name and arguments
|
173
|
+
cache_key = self._create_cache_key(func.__name__, args, kwargs)
|
174
|
+
|
175
|
+
# Try to get from cache
|
176
|
+
result = cache.get(cache_key)
|
177
|
+
if result is not None:
|
178
|
+
logger.debug(f"Cache hit for {func.__name__}: {cache_key}")
|
179
|
+
return result
|
180
|
+
|
181
|
+
# Execute function and cache result
|
182
|
+
logger.debug(f"Cache miss for {func.__name__}: {cache_key}")
|
183
|
+
result = func(*args, **kwargs)
|
184
|
+
cache.set(cache_key, result)
|
185
|
+
return result
|
186
|
+
|
187
|
+
@functools.wraps(func)
|
188
|
+
async def async_wrapper(*args, **kwargs):
|
189
|
+
# Create cache key from function name and arguments
|
190
|
+
cache_key = self._create_cache_key(func.__name__, args, kwargs)
|
191
|
+
|
192
|
+
# Try to get from cache
|
193
|
+
result = cache.get(cache_key)
|
194
|
+
if result is not None:
|
195
|
+
logger.debug(f"Cache hit for {func.__name__}: {cache_key}")
|
196
|
+
return result
|
197
|
+
|
198
|
+
# Execute function and cache result
|
199
|
+
logger.debug(f"Cache miss for {func.__name__}: {cache_key}")
|
200
|
+
result = await func(*args, **kwargs)
|
201
|
+
cache.set(cache_key, result)
|
202
|
+
return result
|
203
|
+
|
204
|
+
# Return appropriate wrapper based on function type
|
205
|
+
if asyncio.iscoroutinefunction(func):
|
206
|
+
return async_wrapper
|
207
|
+
else:
|
208
|
+
return sync_wrapper
|
209
|
+
|
210
|
+
return decorator
|
211
|
+
|
212
|
+
def _create_cache_key(self, func_name: str, args: tuple, kwargs: dict) -> str:
|
213
|
+
"""Create a cache key from function name and arguments."""
|
214
|
+
# Convert args and kwargs to string representation
|
215
|
+
args_str = str(args) if args else ""
|
216
|
+
kwargs_str = str(sorted(kwargs.items())) if kwargs else ""
|
217
|
+
return f"{func_name}:{args_str}:{kwargs_str}"
|
218
|
+
|
219
|
+
def clear_all(self) -> None:
|
220
|
+
"""Clear all caches."""
|
221
|
+
for cache in self._caches.values():
|
222
|
+
cache.clear()
|
223
|
+
|
224
|
+
def stats(self) -> Dict[str, Dict[str, Any]]:
|
225
|
+
"""Get statistics for all caches."""
|
226
|
+
return {name: cache.stats() for name, cache in self._caches.items()}
|
227
|
+
|
228
|
+
|
229
|
+
# Global cache manager instance
|
230
|
+
_global_cache_manager = CacheManager()
|
231
|
+
|
232
|
+
|
233
|
+
def cached_query(cache_name: str = "query", ttl: int = 300, enabled: bool = True):
|
234
|
+
"""
|
235
|
+
Simple decorator for caching query results.
|
236
|
+
|
237
|
+
This is a convenience decorator that uses the global cache manager.
|
238
|
+
|
239
|
+
Args:
|
240
|
+
cache_name: Name of cache to use
|
241
|
+
ttl: Time-to-live for cache entries
|
242
|
+
enabled: Whether caching is enabled
|
243
|
+
|
244
|
+
Example:
|
245
|
+
@cached_query("search", ttl=600)
|
246
|
+
async def search_data(query: str) -> list:
|
247
|
+
# Expensive search operation
|
248
|
+
return results
|
249
|
+
"""
|
250
|
+
|
251
|
+
def decorator(func: F) -> F:
|
252
|
+
if not enabled:
|
253
|
+
return func
|
254
|
+
|
255
|
+
return _global_cache_manager.cached(cache_name, ttl=ttl)(func)
|
256
|
+
|
257
|
+
return decorator
|
258
|
+
|
259
|
+
|
260
|
+
def get_cache_stats() -> Dict[str, Dict[str, Any]]:
|
261
|
+
"""Get statistics for the global cache manager."""
|
262
|
+
return _global_cache_manager.stats()
|
263
|
+
|
264
|
+
|
265
|
+
def clear_all_caches() -> None:
|
266
|
+
"""Clear all caches in the global cache manager."""
|
267
|
+
_global_cache_manager.clear_all()
|
@@ -0,0 +1,263 @@
|
|
1
|
+
"""
|
2
|
+
Configuration management for MCP servers.
|
3
|
+
|
4
|
+
Provides hierarchical configuration with support for:
|
5
|
+
- Default values
|
6
|
+
- YAML/JSON configuration files
|
7
|
+
- Environment variable overrides
|
8
|
+
- Runtime parameter overrides
|
9
|
+
- Dot notation access
|
10
|
+
"""
|
11
|
+
|
12
|
+
import json
|
13
|
+
import logging
|
14
|
+
import os
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Any, Dict, Optional, Union
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
try:
|
21
|
+
import yaml
|
22
|
+
|
23
|
+
HAS_YAML = True
|
24
|
+
except ImportError:
|
25
|
+
HAS_YAML = False
|
26
|
+
logger.warning("PyYAML not available. YAML configuration files not supported.")
|
27
|
+
|
28
|
+
|
29
|
+
class ConfigManager:
|
30
|
+
"""
|
31
|
+
Hierarchical configuration manager for MCP servers.
|
32
|
+
|
33
|
+
Configuration precedence (highest to lowest):
|
34
|
+
1. Runtime overrides (set via set() method)
|
35
|
+
2. Environment variables
|
36
|
+
3. Configuration file (YAML/JSON)
|
37
|
+
4. Default values
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
config_file: Optional[Union[str, Path]] = None,
|
43
|
+
defaults: Optional[Dict[str, Any]] = None,
|
44
|
+
):
|
45
|
+
"""
|
46
|
+
Initialize configuration manager.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
config_file: Path to configuration file (YAML or JSON)
|
50
|
+
defaults: Default configuration values
|
51
|
+
"""
|
52
|
+
self._defaults = defaults or {}
|
53
|
+
self._file_config = {}
|
54
|
+
self._env_config = {}
|
55
|
+
self._runtime_config = {}
|
56
|
+
|
57
|
+
# Load configuration file if provided
|
58
|
+
if config_file:
|
59
|
+
self.load_file(config_file)
|
60
|
+
|
61
|
+
# Load environment variables
|
62
|
+
self._load_env_vars()
|
63
|
+
|
64
|
+
def load_file(self, config_file: Union[str, Path]) -> None:
|
65
|
+
"""Load configuration from file."""
|
66
|
+
config_path = Path(config_file)
|
67
|
+
|
68
|
+
if not config_path.exists():
|
69
|
+
logger.warning(f"Configuration file not found: {config_path}")
|
70
|
+
return
|
71
|
+
|
72
|
+
try:
|
73
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
74
|
+
if config_path.suffix.lower() in [".yaml", ".yml"]:
|
75
|
+
if not HAS_YAML:
|
76
|
+
raise ValueError("PyYAML not available for YAML configuration")
|
77
|
+
self._file_config = yaml.safe_load(f) or {}
|
78
|
+
elif config_path.suffix.lower() == ".json":
|
79
|
+
self._file_config = json.load(f) or {}
|
80
|
+
else:
|
81
|
+
raise ValueError(
|
82
|
+
f"Unsupported configuration file format: {config_path.suffix}"
|
83
|
+
)
|
84
|
+
|
85
|
+
logger.info(f"Loaded configuration from {config_path}")
|
86
|
+
|
87
|
+
except Exception as e:
|
88
|
+
logger.error(f"Failed to load configuration file {config_path}: {e}")
|
89
|
+
self._file_config = {}
|
90
|
+
|
91
|
+
def _load_env_vars(self) -> None:
|
92
|
+
"""Load configuration from environment variables."""
|
93
|
+
# Look for environment variables with MCP_ prefix
|
94
|
+
env_config = {}
|
95
|
+
|
96
|
+
for key, value in os.environ.items():
|
97
|
+
if key.startswith("MCP_"):
|
98
|
+
# Convert MCP_SERVER_NAME to server.name
|
99
|
+
config_key = key[4:].lower().replace("_", ".")
|
100
|
+
|
101
|
+
# Try to parse as JSON, fall back to string
|
102
|
+
try:
|
103
|
+
parsed_value = json.loads(value)
|
104
|
+
except (json.JSONDecodeError, ValueError):
|
105
|
+
parsed_value = value
|
106
|
+
|
107
|
+
self._set_nested_value(env_config, config_key, parsed_value)
|
108
|
+
|
109
|
+
self._env_config = env_config
|
110
|
+
|
111
|
+
def _set_nested_value(self, config: Dict[str, Any], key: str, value: Any) -> None:
|
112
|
+
"""Set a nested configuration value using dot notation."""
|
113
|
+
keys = key.split(".")
|
114
|
+
current = config
|
115
|
+
|
116
|
+
for k in keys[:-1]:
|
117
|
+
if k not in current:
|
118
|
+
current[k] = {}
|
119
|
+
current = current[k]
|
120
|
+
|
121
|
+
current[keys[-1]] = value
|
122
|
+
|
123
|
+
def get(self, key: str, default: Any = None) -> Any:
|
124
|
+
"""
|
125
|
+
Get configuration value using dot notation.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
key: Configuration key (e.g., "server.cache.ttl")
|
129
|
+
default: Default value if key not found
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
Configuration value
|
133
|
+
"""
|
134
|
+
# Check in order of precedence
|
135
|
+
for config in [
|
136
|
+
self._runtime_config,
|
137
|
+
self._env_config,
|
138
|
+
self._file_config,
|
139
|
+
self._defaults,
|
140
|
+
]:
|
141
|
+
value = self._get_nested_value(config, key)
|
142
|
+
if value is not None:
|
143
|
+
return value
|
144
|
+
|
145
|
+
return default
|
146
|
+
|
147
|
+
def _get_nested_value(self, config: Dict[str, Any], key: str) -> Any:
|
148
|
+
"""Get nested value using dot notation."""
|
149
|
+
keys = key.split(".")
|
150
|
+
current = config
|
151
|
+
|
152
|
+
try:
|
153
|
+
for k in keys:
|
154
|
+
current = current[k]
|
155
|
+
return current
|
156
|
+
except (KeyError, TypeError):
|
157
|
+
return None
|
158
|
+
|
159
|
+
def set(self, key: str, value: Any) -> None:
|
160
|
+
"""Set runtime configuration value using dot notation."""
|
161
|
+
self._set_nested_value(self._runtime_config, key, value)
|
162
|
+
|
163
|
+
def update(self, config: Dict[str, Any]) -> None:
|
164
|
+
"""Update runtime configuration with dictionary."""
|
165
|
+
for key, value in config.items():
|
166
|
+
self.set(key, value)
|
167
|
+
|
168
|
+
def to_dict(self) -> Dict[str, Any]:
|
169
|
+
"""Get complete configuration as dictionary."""
|
170
|
+
result = {}
|
171
|
+
|
172
|
+
# Merge all configurations in reverse precedence order
|
173
|
+
for config in [
|
174
|
+
self._defaults,
|
175
|
+
self._file_config,
|
176
|
+
self._env_config,
|
177
|
+
self._runtime_config,
|
178
|
+
]:
|
179
|
+
result = self._deep_merge(result, config)
|
180
|
+
|
181
|
+
return result
|
182
|
+
|
183
|
+
def _deep_merge(
|
184
|
+
self, base: Dict[str, Any], update: Dict[str, Any]
|
185
|
+
) -> Dict[str, Any]:
|
186
|
+
"""Deep merge two dictionaries."""
|
187
|
+
result = base.copy()
|
188
|
+
|
189
|
+
for key, value in update.items():
|
190
|
+
if (
|
191
|
+
key in result
|
192
|
+
and isinstance(result[key], dict)
|
193
|
+
and isinstance(value, dict)
|
194
|
+
):
|
195
|
+
result[key] = self._deep_merge(result[key], value)
|
196
|
+
else:
|
197
|
+
result[key] = value
|
198
|
+
|
199
|
+
return result
|
200
|
+
|
201
|
+
def save(self, config_file: Union[str, Path], format: str = "yaml") -> None:
|
202
|
+
"""
|
203
|
+
Save current configuration to file.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
config_file: Path to save configuration
|
207
|
+
format: File format ('yaml' or 'json')
|
208
|
+
"""
|
209
|
+
config_path = Path(config_file)
|
210
|
+
config_data = self.to_dict()
|
211
|
+
|
212
|
+
try:
|
213
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
214
|
+
if format.lower() == "yaml":
|
215
|
+
if not HAS_YAML:
|
216
|
+
raise ValueError("PyYAML not available for YAML export")
|
217
|
+
yaml.dump(config_data, f, default_flow_style=False, indent=2)
|
218
|
+
elif format.lower() == "json":
|
219
|
+
json.dump(config_data, f, indent=2)
|
220
|
+
else:
|
221
|
+
raise ValueError(f"Unsupported format: {format}")
|
222
|
+
|
223
|
+
logger.info(f"Configuration saved to {config_path}")
|
224
|
+
|
225
|
+
except Exception as e:
|
226
|
+
logger.error(f"Failed to save configuration to {config_path}: {e}")
|
227
|
+
raise
|
228
|
+
|
229
|
+
|
230
|
+
def create_default_config() -> Dict[str, Any]:
|
231
|
+
"""Create default MCP server configuration."""
|
232
|
+
return {
|
233
|
+
"server": {
|
234
|
+
"name": "mcp-server",
|
235
|
+
"version": "1.0.0",
|
236
|
+
"description": "MCP Server",
|
237
|
+
"transport": "stdio",
|
238
|
+
},
|
239
|
+
"cache": {"enabled": True, "default_ttl": 300, "max_size": 128},
|
240
|
+
"metrics": {
|
241
|
+
"enabled": True,
|
242
|
+
"collect_performance": True,
|
243
|
+
"collect_usage": True,
|
244
|
+
},
|
245
|
+
"logging": {
|
246
|
+
"level": "INFO",
|
247
|
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
248
|
+
},
|
249
|
+
}
|
250
|
+
|
251
|
+
|
252
|
+
def load_config_file(config_file: Union[str, Path]) -> ConfigManager:
|
253
|
+
"""
|
254
|
+
Convenience function to load configuration from file.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
config_file: Path to configuration file
|
258
|
+
|
259
|
+
Returns:
|
260
|
+
ConfigManager instance with loaded configuration
|
261
|
+
"""
|
262
|
+
defaults = create_default_config()
|
263
|
+
return ConfigManager(config_file=config_file, defaults=defaults)
|