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.
Files changed (43) hide show
  1. common/__init__.py +21 -0
  2. common/types.py +182 -0
  3. lightfast_mcp/__init__.py +50 -0
  4. lightfast_mcp/core/__init__.py +14 -0
  5. lightfast_mcp/core/base_server.py +205 -0
  6. lightfast_mcp/exceptions.py +55 -0
  7. lightfast_mcp/servers/__init__.py +1 -0
  8. lightfast_mcp/servers/blender/__init__.py +5 -0
  9. lightfast_mcp/servers/blender/server.py +358 -0
  10. lightfast_mcp/servers/blender_mcp_server.py +82 -0
  11. lightfast_mcp/servers/mock/__init__.py +5 -0
  12. lightfast_mcp/servers/mock/server.py +101 -0
  13. lightfast_mcp/servers/mock/tools.py +161 -0
  14. lightfast_mcp/servers/mock_server.py +78 -0
  15. lightfast_mcp/utils/__init__.py +1 -0
  16. lightfast_mcp/utils/logging_utils.py +69 -0
  17. mseep_lightfast_mcp-0.0.1.dist-info/METADATA +36 -0
  18. mseep_lightfast_mcp-0.0.1.dist-info/RECORD +43 -0
  19. mseep_lightfast_mcp-0.0.1.dist-info/WHEEL +5 -0
  20. mseep_lightfast_mcp-0.0.1.dist-info/entry_points.txt +7 -0
  21. mseep_lightfast_mcp-0.0.1.dist-info/licenses/LICENSE +21 -0
  22. mseep_lightfast_mcp-0.0.1.dist-info/top_level.txt +3 -0
  23. tools/__init__.py +46 -0
  24. tools/ai/__init__.py +8 -0
  25. tools/ai/conversation_cli.py +345 -0
  26. tools/ai/conversation_client.py +399 -0
  27. tools/ai/conversation_session.py +342 -0
  28. tools/ai/providers/__init__.py +11 -0
  29. tools/ai/providers/base_provider.py +64 -0
  30. tools/ai/providers/claude_provider.py +200 -0
  31. tools/ai/providers/openai_provider.py +204 -0
  32. tools/ai/tool_executor.py +257 -0
  33. tools/common/__init__.py +99 -0
  34. tools/common/async_utils.py +419 -0
  35. tools/common/errors.py +222 -0
  36. tools/common/logging.py +252 -0
  37. tools/common/types.py +130 -0
  38. tools/orchestration/__init__.py +15 -0
  39. tools/orchestration/cli.py +320 -0
  40. tools/orchestration/config_loader.py +348 -0
  41. tools/orchestration/server_orchestrator.py +466 -0
  42. tools/orchestration/server_registry.py +187 -0
  43. 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