mseep-lightfast-mcp 0.0.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.
- common/__init__.py +21 -0
- common/types.py +182 -0
- lightfast_mcp/__init__.py +50 -0
- lightfast_mcp/core/__init__.py +14 -0
- lightfast_mcp/core/base_server.py +205 -0
- lightfast_mcp/exceptions.py +55 -0
- lightfast_mcp/servers/__init__.py +1 -0
- lightfast_mcp/servers/blender/__init__.py +5 -0
- lightfast_mcp/servers/blender/server.py +358 -0
- lightfast_mcp/servers/blender_mcp_server.py +82 -0
- lightfast_mcp/servers/mock/__init__.py +5 -0
- lightfast_mcp/servers/mock/server.py +101 -0
- lightfast_mcp/servers/mock/tools.py +161 -0
- lightfast_mcp/servers/mock_server.py +78 -0
- lightfast_mcp/utils/__init__.py +1 -0
- lightfast_mcp/utils/logging_utils.py +69 -0
- mseep_lightfast_mcp-0.0.1.dist-info/METADATA +36 -0
- mseep_lightfast_mcp-0.0.1.dist-info/RECORD +43 -0
- mseep_lightfast_mcp-0.0.1.dist-info/WHEEL +5 -0
- mseep_lightfast_mcp-0.0.1.dist-info/entry_points.txt +7 -0
- mseep_lightfast_mcp-0.0.1.dist-info/licenses/LICENSE +21 -0
- mseep_lightfast_mcp-0.0.1.dist-info/top_level.txt +3 -0
- tools/__init__.py +46 -0
- tools/ai/__init__.py +8 -0
- tools/ai/conversation_cli.py +345 -0
- tools/ai/conversation_client.py +399 -0
- tools/ai/conversation_session.py +342 -0
- tools/ai/providers/__init__.py +11 -0
- tools/ai/providers/base_provider.py +64 -0
- tools/ai/providers/claude_provider.py +200 -0
- tools/ai/providers/openai_provider.py +204 -0
- tools/ai/tool_executor.py +257 -0
- tools/common/__init__.py +99 -0
- tools/common/async_utils.py +419 -0
- tools/common/errors.py +222 -0
- tools/common/logging.py +252 -0
- tools/common/types.py +130 -0
- tools/orchestration/__init__.py +15 -0
- tools/orchestration/cli.py +320 -0
- tools/orchestration/config_loader.py +348 -0
- tools/orchestration/server_orchestrator.py +466 -0
- tools/orchestration/server_registry.py +187 -0
- tools/orchestration/server_selector.py +242 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Lightfast MCP Orchestrator - Multi-server management for creative applications.
|
|
4
|
+
|
|
5
|
+
This is the main entry point for orchestrating multiple MCP servers simultaneously.
|
|
6
|
+
Users can select which servers to start, run them in the background, and then
|
|
7
|
+
use the dedicated AI client to interact with them.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import asyncio
|
|
12
|
+
|
|
13
|
+
from lightfast_mcp.utils.logging_utils import configure_logging, get_logger
|
|
14
|
+
|
|
15
|
+
from .config_loader import ConfigLoader
|
|
16
|
+
from .server_orchestrator import get_orchestrator
|
|
17
|
+
from .server_selector import ServerSelector
|
|
18
|
+
|
|
19
|
+
# Configure logging
|
|
20
|
+
configure_logging(level="INFO")
|
|
21
|
+
logger = get_logger("LightfastMCPOrchestrator")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Async wrapper functions for CLI
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def start_multiple_servers_sync(configs, background=True, show_logs=True):
|
|
28
|
+
"""Sync wrapper for async start_multiple_servers."""
|
|
29
|
+
orchestrator = get_orchestrator()
|
|
30
|
+
|
|
31
|
+
async def _async_start():
|
|
32
|
+
result = await orchestrator.start_multiple_servers(
|
|
33
|
+
configs, background, show_logs
|
|
34
|
+
)
|
|
35
|
+
return result.data if result.is_success else {}
|
|
36
|
+
|
|
37
|
+
return asyncio.run(_async_start())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_server_urls_sync():
|
|
41
|
+
"""Get server URLs from orchestrator."""
|
|
42
|
+
orchestrator = get_orchestrator()
|
|
43
|
+
servers = orchestrator.get_running_servers()
|
|
44
|
+
return {name: info.url for name, info in servers.items() if info.url}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def wait_for_shutdown_sync():
|
|
48
|
+
"""Wait for shutdown signal."""
|
|
49
|
+
orchestrator = get_orchestrator()
|
|
50
|
+
orchestrator._shutdown_event.wait()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def shutdown_all_sync():
|
|
54
|
+
"""Shutdown all servers."""
|
|
55
|
+
orchestrator = get_orchestrator()
|
|
56
|
+
orchestrator.shutdown_all()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_sample_config():
|
|
60
|
+
"""Create a sample configuration file."""
|
|
61
|
+
print("[CONFIG] Creating sample configuration...")
|
|
62
|
+
|
|
63
|
+
config_loader = ConfigLoader()
|
|
64
|
+
success = config_loader.create_sample_config("servers.yaml")
|
|
65
|
+
|
|
66
|
+
if success:
|
|
67
|
+
print("[OK] Sample configuration created at: config/servers.yaml")
|
|
68
|
+
print("[INFO] Edit this file to customize your server settings.")
|
|
69
|
+
print("[START] Run 'lightfast-mcp-orchestrator start' to begin!")
|
|
70
|
+
else:
|
|
71
|
+
print("[ERROR] Failed to create sample configuration")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_available_servers():
|
|
75
|
+
"""List all available server types and configurations."""
|
|
76
|
+
print("[INFO] Available Server Types:")
|
|
77
|
+
print("=" * 50)
|
|
78
|
+
|
|
79
|
+
from .server_registry import get_registry
|
|
80
|
+
|
|
81
|
+
registry = get_registry()
|
|
82
|
+
server_info = registry.get_server_info()
|
|
83
|
+
|
|
84
|
+
for server_type, info in server_info.items():
|
|
85
|
+
print(f"[SERVER] {server_type}")
|
|
86
|
+
print(f" Version: {info['version']}")
|
|
87
|
+
print(f" Description: {info['description']}")
|
|
88
|
+
if info["required_dependencies"]:
|
|
89
|
+
print(f" Dependencies: {', '.join(info['required_dependencies'])}")
|
|
90
|
+
if info["required_apps"]:
|
|
91
|
+
print(f" Required Apps: {', '.join(info['required_apps'])}")
|
|
92
|
+
print()
|
|
93
|
+
|
|
94
|
+
print("[CONFIG] Server Configurations:")
|
|
95
|
+
print("=" * 50)
|
|
96
|
+
|
|
97
|
+
config_loader = ConfigLoader()
|
|
98
|
+
configs = config_loader.load_servers_config()
|
|
99
|
+
|
|
100
|
+
if not configs:
|
|
101
|
+
print("[ERROR] No server configurations found.")
|
|
102
|
+
print(
|
|
103
|
+
" Run 'lightfast-mcp-orchestrator init' to create a sample configuration."
|
|
104
|
+
)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
for config in configs:
|
|
108
|
+
server_type = config.config.get("type", "unknown")
|
|
109
|
+
print(f"[SERVER] {config.name} ({server_type})")
|
|
110
|
+
print(f" Description: {config.description}")
|
|
111
|
+
print(f" Transport: {config.transport}")
|
|
112
|
+
if config.transport in ["http", "streamable-http"]:
|
|
113
|
+
print(f" URL: http://{config.host}:{config.port}{config.path}")
|
|
114
|
+
print()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def start_servers_interactive(show_logs: bool = True):
|
|
118
|
+
"""Start servers with interactive selection."""
|
|
119
|
+
print("[START] Lightfast MCP Multi-Server Orchestrator")
|
|
120
|
+
print("=" * 50)
|
|
121
|
+
|
|
122
|
+
# Interactive server selection
|
|
123
|
+
selector = ServerSelector()
|
|
124
|
+
selected_configs = selector.load_available_servers()
|
|
125
|
+
|
|
126
|
+
if not selected_configs:
|
|
127
|
+
print("[ERROR] No server configurations found.")
|
|
128
|
+
print(" Would you like to create a sample configuration? (y/n)")
|
|
129
|
+
try:
|
|
130
|
+
create_sample = input().strip().lower()
|
|
131
|
+
if create_sample in ["y", "yes"]:
|
|
132
|
+
config_loader = ConfigLoader()
|
|
133
|
+
if config_loader.create_sample_config("servers.yaml"):
|
|
134
|
+
print("[OK] Sample configuration created at: config/servers.yaml")
|
|
135
|
+
print("[INFO] Loading the new configuration...")
|
|
136
|
+
selected_configs = selector.load_available_servers()
|
|
137
|
+
else:
|
|
138
|
+
print("[ERROR] Failed to create sample configuration")
|
|
139
|
+
return
|
|
140
|
+
else:
|
|
141
|
+
print("[BYE] No configuration created. Goodbye!")
|
|
142
|
+
return
|
|
143
|
+
except KeyboardInterrupt:
|
|
144
|
+
print("\n[BYE] Cancelled. Goodbye!")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if not selected_configs:
|
|
148
|
+
print("[ERROR] Still no server configurations available.")
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# Let user select servers
|
|
152
|
+
selected_configs = selector.select_servers_interactive()
|
|
153
|
+
|
|
154
|
+
if not selected_configs:
|
|
155
|
+
print("[BYE] No servers selected. Goodbye!")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
print(f"\n[START] Starting {len(selected_configs)} servers...")
|
|
159
|
+
print(" This may take a few moments as servers initialize...")
|
|
160
|
+
|
|
161
|
+
results = start_multiple_servers_sync(
|
|
162
|
+
selected_configs, background=True, show_logs=show_logs
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Show results
|
|
166
|
+
successful = sum(1 for success in results.values() if success)
|
|
167
|
+
print(f"[OK] Successfully started {successful}/{len(selected_configs)} servers")
|
|
168
|
+
|
|
169
|
+
# Show any failures
|
|
170
|
+
failed_servers = [name for name, success in results.items() if not success]
|
|
171
|
+
if failed_servers:
|
|
172
|
+
print(f"[ERROR] Failed to start: {', '.join(failed_servers)}")
|
|
173
|
+
|
|
174
|
+
if successful > 0:
|
|
175
|
+
# Show server URLs
|
|
176
|
+
urls = get_server_urls_sync()
|
|
177
|
+
if urls:
|
|
178
|
+
print("\n[URLS] Server URLs:")
|
|
179
|
+
for name, url in urls.items():
|
|
180
|
+
print(f" • {name}: {url}")
|
|
181
|
+
|
|
182
|
+
print(
|
|
183
|
+
"\n[INFO] Servers are running! Use the dedicated AI client to interact with them."
|
|
184
|
+
)
|
|
185
|
+
print(
|
|
186
|
+
" Run 'uv run lightfast-conversation-client chat' to start the AI client."
|
|
187
|
+
)
|
|
188
|
+
print(" Press Ctrl+C to shutdown all servers.\n")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Wait for shutdown
|
|
192
|
+
wait_for_shutdown_sync()
|
|
193
|
+
except KeyboardInterrupt:
|
|
194
|
+
print("\n[STOP] Shutting down servers...")
|
|
195
|
+
shutdown_all_sync()
|
|
196
|
+
print("[BYE] All servers stopped. Goodbye!")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def start_servers_by_names(server_names: list[str], show_logs: bool = True):
|
|
200
|
+
"""Start specific servers by name."""
|
|
201
|
+
config_loader = ConfigLoader()
|
|
202
|
+
all_configs = config_loader.load_servers_config()
|
|
203
|
+
|
|
204
|
+
if not all_configs:
|
|
205
|
+
print("[ERROR] No server configurations found.")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Find requested servers
|
|
209
|
+
selected_configs = []
|
|
210
|
+
for name in server_names:
|
|
211
|
+
config = next((c for c in all_configs if c.name == name), None)
|
|
212
|
+
if config:
|
|
213
|
+
selected_configs.append(config)
|
|
214
|
+
else:
|
|
215
|
+
print(f"[WARN] Server configuration not found: {name}")
|
|
216
|
+
|
|
217
|
+
if not selected_configs:
|
|
218
|
+
print("[ERROR] No valid servers to start.")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# Start servers
|
|
222
|
+
results = start_multiple_servers_sync(
|
|
223
|
+
selected_configs, background=True, show_logs=show_logs
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Show results
|
|
227
|
+
successful = sum(1 for success in results.values() if success)
|
|
228
|
+
print(f"[OK] Successfully started {successful}/{len(selected_configs)} servers")
|
|
229
|
+
|
|
230
|
+
if successful > 0:
|
|
231
|
+
urls = get_server_urls_sync()
|
|
232
|
+
if urls:
|
|
233
|
+
print("\n[URLS] Server URLs:")
|
|
234
|
+
for name, url in urls.items():
|
|
235
|
+
print(f" • {name}: {url}")
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
wait_for_shutdown_sync()
|
|
239
|
+
except KeyboardInterrupt:
|
|
240
|
+
print("\n[STOP] Shutting down servers...")
|
|
241
|
+
shutdown_all_sync()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main():
|
|
245
|
+
"""Main CLI entry point."""
|
|
246
|
+
parser = argparse.ArgumentParser(
|
|
247
|
+
description="Lightfast MCP Orchestrator - Multi-server management for creative applications",
|
|
248
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
249
|
+
epilog="""
|
|
250
|
+
Examples:
|
|
251
|
+
lightfast-mcp-orchestrator init # Create sample configuration
|
|
252
|
+
lightfast-mcp-orchestrator list # List available servers
|
|
253
|
+
lightfast-mcp-orchestrator start # Interactive server selection
|
|
254
|
+
lightfast-mcp-orchestrator start blender-server # Start specific server
|
|
255
|
+
lightfast-mcp-orchestrator start --hide-logs # Start servers without showing logs
|
|
256
|
+
lightfast-mcp-orchestrator start --verbose # Start with debug logging and server logs
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
AI Client (use after starting servers):
|
|
261
|
+
uv run lightfast-conversation-client chat # Start interactive AI chat
|
|
262
|
+
uv run lightfast-conversation-client test # Quick AI test
|
|
263
|
+
""",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
parser.add_argument(
|
|
267
|
+
"command", choices=["init", "list", "start"], help="Command to run"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
parser.add_argument(
|
|
271
|
+
"servers", nargs="*", help="Server names to start (for 'start' command)"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
parser.add_argument("--config", help="Configuration file path")
|
|
275
|
+
|
|
276
|
+
parser.add_argument(
|
|
277
|
+
"--verbose", "-v", action="store_true", help="Enable verbose logging"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
parser.add_argument(
|
|
281
|
+
"--show-logs",
|
|
282
|
+
action="store_true",
|
|
283
|
+
default=True,
|
|
284
|
+
help="Show server logs in terminal (default: True)",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
parser.add_argument(
|
|
288
|
+
"--hide-logs", action="store_true", help="Hide server logs from terminal"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
args = parser.parse_args()
|
|
292
|
+
|
|
293
|
+
# Set logging level
|
|
294
|
+
if args.verbose:
|
|
295
|
+
configure_logging(level="DEBUG")
|
|
296
|
+
print("[DEBUG] Debug logging enabled")
|
|
297
|
+
|
|
298
|
+
# Determine log visibility (--hide-logs takes precedence)
|
|
299
|
+
show_logs = not args.hide_logs if args.hide_logs else args.show_logs
|
|
300
|
+
if args.verbose:
|
|
301
|
+
print(
|
|
302
|
+
f"[INFO] Server logs visibility: {'Enabled' if show_logs else 'Disabled'}"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Handle commands
|
|
306
|
+
if args.command == "init":
|
|
307
|
+
create_sample_config()
|
|
308
|
+
|
|
309
|
+
elif args.command == "list":
|
|
310
|
+
list_available_servers()
|
|
311
|
+
|
|
312
|
+
elif args.command == "start":
|
|
313
|
+
if args.servers:
|
|
314
|
+
start_servers_by_names(args.servers, show_logs=show_logs)
|
|
315
|
+
else:
|
|
316
|
+
start_servers_interactive(show_logs=show_logs)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
main()
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Configuration loader for MCP servers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
YAML_AVAILABLE = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
YAML_AVAILABLE = False
|
|
14
|
+
|
|
15
|
+
from lightfast_mcp.core.base_server import ServerConfig
|
|
16
|
+
from lightfast_mcp.utils.logging_utils import get_logger
|
|
17
|
+
|
|
18
|
+
logger = get_logger("ConfigLoader")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConfigLoader:
|
|
22
|
+
"""Loader for server configurations from files."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config_dir: str | Path | None = None):
|
|
25
|
+
"""Initialize the config loader."""
|
|
26
|
+
self.config_dir = Path(config_dir) if config_dir else Path.cwd() / "config"
|
|
27
|
+
|
|
28
|
+
# Ensure config directory exists
|
|
29
|
+
self.config_dir.mkdir(exist_ok=True)
|
|
30
|
+
|
|
31
|
+
logger.info(f"Config directory: {self.config_dir}")
|
|
32
|
+
|
|
33
|
+
def load_servers_config(
|
|
34
|
+
self, config_file: str | Path | None = None
|
|
35
|
+
) -> list[ServerConfig]:
|
|
36
|
+
"""Load server configurations from a file."""
|
|
37
|
+
if config_file is None:
|
|
38
|
+
# Look for default config files
|
|
39
|
+
config_file = self._find_default_config()
|
|
40
|
+
|
|
41
|
+
if not config_file:
|
|
42
|
+
logger.warning("No configuration file found, returning empty list")
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
config_path = Path(config_file)
|
|
46
|
+
if not config_path.is_absolute():
|
|
47
|
+
config_path = self.config_dir / config_path
|
|
48
|
+
|
|
49
|
+
if not config_path.exists():
|
|
50
|
+
logger.error(f"Configuration file not found: {config_path}")
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
logger.info(f"Loading server configurations from: {config_path}")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
if config_path.suffix.lower() in [".yaml", ".yml"]:
|
|
57
|
+
return self._load_yaml_config(config_path)
|
|
58
|
+
elif config_path.suffix.lower() == ".json":
|
|
59
|
+
return self._load_json_config(config_path)
|
|
60
|
+
else:
|
|
61
|
+
logger.error(f"Unsupported config file format: {config_path.suffix}")
|
|
62
|
+
return []
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Error loading configuration: {e}")
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
def _find_default_config(self) -> Path | None:
|
|
68
|
+
"""Find the default configuration file."""
|
|
69
|
+
possible_files = [
|
|
70
|
+
"servers.yaml",
|
|
71
|
+
"servers.yml",
|
|
72
|
+
"servers.json",
|
|
73
|
+
"lightfast-mcp.yaml",
|
|
74
|
+
"lightfast-mcp.yml",
|
|
75
|
+
"lightfast-mcp.json",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
for filename in possible_files:
|
|
79
|
+
config_path = self.config_dir / filename
|
|
80
|
+
if config_path.exists():
|
|
81
|
+
logger.info(f"Found default config file: {config_path}")
|
|
82
|
+
return config_path
|
|
83
|
+
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def _load_yaml_config(self, config_path: Path) -> list[ServerConfig]:
|
|
87
|
+
"""Load configuration from YAML file."""
|
|
88
|
+
if not YAML_AVAILABLE:
|
|
89
|
+
raise ImportError(
|
|
90
|
+
"PyYAML is required to load YAML configuration files. Install with: pip install pyyaml"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
with open(config_path, encoding="utf-8") as f:
|
|
94
|
+
data = yaml.safe_load(f)
|
|
95
|
+
|
|
96
|
+
return self._parse_config_data(data)
|
|
97
|
+
|
|
98
|
+
def _load_json_config(self, config_path: Path) -> list[ServerConfig]:
|
|
99
|
+
"""Load configuration from JSON file."""
|
|
100
|
+
with open(config_path, encoding="utf-8") as f:
|
|
101
|
+
data = json.load(f)
|
|
102
|
+
|
|
103
|
+
return self._parse_config_data(data)
|
|
104
|
+
|
|
105
|
+
def _parse_config_data(self, data: dict[str, Any]) -> list[ServerConfig]:
|
|
106
|
+
"""Parse configuration data into ServerConfig objects."""
|
|
107
|
+
if not isinstance(data, dict):
|
|
108
|
+
raise ValueError("Configuration must be a dictionary")
|
|
109
|
+
|
|
110
|
+
servers_data = data.get("servers", [])
|
|
111
|
+
if not isinstance(servers_data, list):
|
|
112
|
+
raise ValueError("'servers' must be a list")
|
|
113
|
+
|
|
114
|
+
server_configs = []
|
|
115
|
+
|
|
116
|
+
for i, server_data in enumerate(servers_data):
|
|
117
|
+
try:
|
|
118
|
+
server_config = self._parse_server_config(server_data)
|
|
119
|
+
server_configs.append(server_config)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error parsing server config at index {i}: {e}")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
logger.info(f"Loaded {len(server_configs)} server configurations")
|
|
125
|
+
return server_configs
|
|
126
|
+
|
|
127
|
+
def _parse_server_config(self, server_data: dict[str, Any]) -> ServerConfig:
|
|
128
|
+
"""Parse a single server configuration."""
|
|
129
|
+
if not isinstance(server_data, dict):
|
|
130
|
+
raise ValueError("Server configuration must be a dictionary")
|
|
131
|
+
|
|
132
|
+
# Required fields
|
|
133
|
+
name = server_data.get("name")
|
|
134
|
+
if not name:
|
|
135
|
+
raise ValueError("Server 'name' is required")
|
|
136
|
+
|
|
137
|
+
description = server_data.get("description", f"{name} MCP Server")
|
|
138
|
+
|
|
139
|
+
# Optional fields with defaults
|
|
140
|
+
version = server_data.get("version", "1.0.0")
|
|
141
|
+
host = server_data.get("host", "localhost")
|
|
142
|
+
port = server_data.get("port", 8000)
|
|
143
|
+
transport = server_data.get("transport", "stdio")
|
|
144
|
+
path = server_data.get("path", "/mcp")
|
|
145
|
+
|
|
146
|
+
# Server-specific configuration
|
|
147
|
+
config = server_data.get("config", {})
|
|
148
|
+
|
|
149
|
+
# Add server type to config if not present
|
|
150
|
+
if "type" not in config:
|
|
151
|
+
# Try to infer from name or set a default
|
|
152
|
+
config["type"] = server_data.get("type", "unknown")
|
|
153
|
+
|
|
154
|
+
# Dependencies and requirements
|
|
155
|
+
dependencies = server_data.get("dependencies", [])
|
|
156
|
+
required_apps = server_data.get("required_apps", [])
|
|
157
|
+
|
|
158
|
+
return ServerConfig(
|
|
159
|
+
name=name,
|
|
160
|
+
description=description,
|
|
161
|
+
version=version,
|
|
162
|
+
host=host,
|
|
163
|
+
port=port,
|
|
164
|
+
transport=transport,
|
|
165
|
+
path=path,
|
|
166
|
+
config=config,
|
|
167
|
+
dependencies=dependencies,
|
|
168
|
+
required_apps=required_apps,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def save_servers_config(
|
|
172
|
+
self, server_configs: list[ServerConfig], config_file: str | Path | None = None
|
|
173
|
+
) -> bool:
|
|
174
|
+
"""Save server configurations to a file."""
|
|
175
|
+
if config_file is None:
|
|
176
|
+
config_file = self.config_dir / "servers.yaml"
|
|
177
|
+
else:
|
|
178
|
+
config_file = Path(config_file)
|
|
179
|
+
if not config_file.is_absolute():
|
|
180
|
+
config_file = self.config_dir / config_file
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Convert server configs to dictionary format
|
|
184
|
+
data = {
|
|
185
|
+
"servers": [
|
|
186
|
+
self._server_config_to_dict(config) for config in server_configs
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Save based on file extension
|
|
191
|
+
if config_file.suffix.lower() in [".yaml", ".yml"]:
|
|
192
|
+
self._save_yaml_config(config_file, data)
|
|
193
|
+
elif config_file.suffix.lower() == ".json":
|
|
194
|
+
self._save_json_config(config_file, data)
|
|
195
|
+
else:
|
|
196
|
+
logger.error(
|
|
197
|
+
f"Unsupported config file format for saving: {config_file.suffix}"
|
|
198
|
+
)
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
f"Saved {len(server_configs)} server configurations to: {config_file}"
|
|
203
|
+
)
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error(f"Error saving configuration: {e}")
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def _server_config_to_dict(self, server_config: ServerConfig) -> dict[str, Any]:
|
|
211
|
+
"""Convert ServerConfig to dictionary."""
|
|
212
|
+
return {
|
|
213
|
+
"name": server_config.name,
|
|
214
|
+
"description": server_config.description,
|
|
215
|
+
"version": server_config.version,
|
|
216
|
+
"type": server_config.config.get("type", "unknown"),
|
|
217
|
+
"host": server_config.host,
|
|
218
|
+
"port": server_config.port,
|
|
219
|
+
"transport": server_config.transport,
|
|
220
|
+
"path": server_config.path,
|
|
221
|
+
"config": server_config.config,
|
|
222
|
+
"dependencies": server_config.dependencies,
|
|
223
|
+
"required_apps": server_config.required_apps,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
def _save_yaml_config(self, config_file: Path, data: dict[str, Any]):
|
|
227
|
+
"""Save configuration to YAML file."""
|
|
228
|
+
if not YAML_AVAILABLE:
|
|
229
|
+
raise ImportError(
|
|
230
|
+
"PyYAML is required to save YAML configuration files. Install with: pip install pyyaml"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
with open(config_file, "w", encoding="utf-8") as f:
|
|
234
|
+
yaml.dump(data, f, default_flow_style=False, indent=2)
|
|
235
|
+
|
|
236
|
+
def _save_json_config(self, config_file: Path, data: dict[str, Any]):
|
|
237
|
+
"""Save configuration to JSON file."""
|
|
238
|
+
with open(config_file, "w", encoding="utf-8") as f:
|
|
239
|
+
json.dump(data, f, indent=2)
|
|
240
|
+
|
|
241
|
+
def create_sample_config(self, config_file: str | Path | None = None) -> bool:
|
|
242
|
+
"""Create a sample configuration file."""
|
|
243
|
+
sample_configs = [
|
|
244
|
+
ServerConfig(
|
|
245
|
+
name="blender-server",
|
|
246
|
+
description="Blender MCP Server for 3D modeling and animation",
|
|
247
|
+
version="1.0.0",
|
|
248
|
+
host="localhost",
|
|
249
|
+
port=8001,
|
|
250
|
+
transport="streamable-http",
|
|
251
|
+
path="/mcp",
|
|
252
|
+
config={
|
|
253
|
+
"type": "blender",
|
|
254
|
+
"blender_host": "localhost",
|
|
255
|
+
"blender_port": 9876,
|
|
256
|
+
},
|
|
257
|
+
dependencies=[],
|
|
258
|
+
required_apps=["Blender"],
|
|
259
|
+
),
|
|
260
|
+
ServerConfig(
|
|
261
|
+
name="mock-server",
|
|
262
|
+
description="Mock MCP Server for testing and development",
|
|
263
|
+
version="1.0.0",
|
|
264
|
+
host="localhost",
|
|
265
|
+
port=8002,
|
|
266
|
+
transport="streamable-http",
|
|
267
|
+
path="/mcp",
|
|
268
|
+
config={
|
|
269
|
+
"type": "mock",
|
|
270
|
+
},
|
|
271
|
+
dependencies=[],
|
|
272
|
+
required_apps=[],
|
|
273
|
+
),
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
if config_file is None:
|
|
277
|
+
config_file = self.config_dir / "servers.yaml"
|
|
278
|
+
|
|
279
|
+
return self.save_servers_config(sample_configs, config_file)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# Environment variable support
|
|
283
|
+
def load_config_from_env() -> list[ServerConfig]:
|
|
284
|
+
"""Load configuration from environment variables."""
|
|
285
|
+
configs = []
|
|
286
|
+
|
|
287
|
+
# Check for environment-based configuration
|
|
288
|
+
env_config = os.getenv("LIGHTFAST_MCP_SERVERS")
|
|
289
|
+
if env_config:
|
|
290
|
+
try:
|
|
291
|
+
data = json.loads(env_config)
|
|
292
|
+
loader = ConfigLoader()
|
|
293
|
+
configs = loader._parse_config_data(data)
|
|
294
|
+
logger.info(f"Loaded {len(configs)} server configs from environment")
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Error parsing environment configuration: {e}")
|
|
297
|
+
|
|
298
|
+
return configs
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def load_server_configs(
|
|
302
|
+
config_path: str | Path | None = None,
|
|
303
|
+
) -> dict[str, dict[str, Any]]:
|
|
304
|
+
"""Convenience function to load server configs in the format expected by ConversationClient."""
|
|
305
|
+
# If config_path is provided and starts with 'config/', treat it as relative to project root
|
|
306
|
+
if config_path and str(config_path).startswith("config/"):
|
|
307
|
+
# Don't create a ConfigLoader with config_dir, let it be relative to current directory
|
|
308
|
+
loader = ConfigLoader(config_dir=Path.cwd())
|
|
309
|
+
server_configs = loader.load_servers_config(config_path)
|
|
310
|
+
else:
|
|
311
|
+
# Use default behavior
|
|
312
|
+
loader = ConfigLoader()
|
|
313
|
+
server_configs = loader.load_servers_config(config_path)
|
|
314
|
+
|
|
315
|
+
# Convert ServerConfig objects to dictionary format expected by ConversationClient
|
|
316
|
+
servers = {}
|
|
317
|
+
for config in server_configs:
|
|
318
|
+
server_dict = {
|
|
319
|
+
"name": config.name,
|
|
320
|
+
"version": config.version,
|
|
321
|
+
"type": config.transport, # Use transport type for connection
|
|
322
|
+
"host": config.host,
|
|
323
|
+
"port": config.port,
|
|
324
|
+
"path": config.path,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# For stdio transport, we need command and args
|
|
328
|
+
if config.transport == "stdio":
|
|
329
|
+
# Try to get from config, otherwise use defaults
|
|
330
|
+
server_dict["command"] = config.config.get(
|
|
331
|
+
"command", f"lightfast-{config.name.replace('-', '_')}"
|
|
332
|
+
)
|
|
333
|
+
server_dict["args"] = config.config.get("args", [])
|
|
334
|
+
elif config.transport in ["sse", "streamable-http"]:
|
|
335
|
+
# For HTTP-based transports, construct URL
|
|
336
|
+
server_dict["url"] = f"http://{config.host}:{config.port}{config.path}"
|
|
337
|
+
# Map streamable-http to sse for MCP client
|
|
338
|
+
if config.transport == "streamable-http":
|
|
339
|
+
server_dict["type"] = "sse"
|
|
340
|
+
|
|
341
|
+
# Add any additional config (but don't override the type we set above)
|
|
342
|
+
for key, value in config.config.items():
|
|
343
|
+
if key != "type": # Don't override the transport type
|
|
344
|
+
server_dict[key] = value
|
|
345
|
+
|
|
346
|
+
servers[config.name] = server_dict
|
|
347
|
+
|
|
348
|
+
return servers
|