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,466 @@
1
+ """Server orchestrator for managing multiple MCP servers simultaneously."""
2
+
3
+ import signal
4
+ import subprocess
5
+ import threading
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from typing import Dict, List, Optional
9
+
10
+ from lightfast_mcp.core.base_server import BaseServer, ServerConfig
11
+ from tools.common import (
12
+ OperationStatus,
13
+ Result,
14
+ RetryManager,
15
+ ServerInfo,
16
+ ServerStartupError,
17
+ ServerState,
18
+ get_logger,
19
+ run_concurrent_operations,
20
+ with_correlation_id,
21
+ with_operation_context,
22
+ )
23
+
24
+ from .server_registry import get_registry
25
+
26
+ logger = get_logger("ServerOrchestrator")
27
+
28
+
29
+ @dataclass
30
+ class ServerProcess:
31
+ """Information about a running server process."""
32
+
33
+ server: Optional[BaseServer] = None
34
+ thread: Optional[threading.Thread] = None
35
+ process: Optional[subprocess.Popen] = None
36
+ process_id: Optional[int] = None
37
+ start_time: datetime = field(default_factory=datetime.utcnow)
38
+ is_background: bool = False
39
+ config: Optional[ServerConfig] = None
40
+
41
+ @property
42
+ def uptime_seconds(self) -> float:
43
+ return (datetime.utcnow() - self.start_time).total_seconds()
44
+
45
+
46
+ class ServerOrchestrator:
47
+ """Orchestrates lifecycle and coordination of multiple MCP servers."""
48
+
49
+ def __init__(self, max_concurrent_startups: int = 3):
50
+ """Initialize the server orchestrator."""
51
+ self.registry = get_registry()
52
+ self._running_servers: Dict[str, ServerProcess] = {}
53
+ self._shutdown_event = threading.Event()
54
+ self.max_concurrent_startups = max_concurrent_startups
55
+ self.retry_manager = RetryManager(max_attempts=3, base_delay=2.0)
56
+
57
+ # Setup signal handlers for graceful shutdown
58
+ self._setup_signal_handlers()
59
+
60
+ def _setup_signal_handlers(self):
61
+ """Setup signal handlers for graceful shutdown."""
62
+
63
+ def signal_handler(signum, frame):
64
+ logger.info(f"Received signal {signum}, initiating graceful shutdown")
65
+ self.shutdown_all()
66
+
67
+ try:
68
+ signal.signal(signal.SIGINT, signal_handler)
69
+ signal.signal(signal.SIGTERM, signal_handler)
70
+ except ValueError:
71
+ # Signal handling might not work in some environments (like Jupyter)
72
+ logger.debug("Could not setup signal handlers")
73
+
74
+ @with_correlation_id
75
+ @with_operation_context(operation="start_server")
76
+ async def start_server(
77
+ self,
78
+ server_config: ServerConfig,
79
+ background: bool = False,
80
+ show_logs: bool = True,
81
+ ) -> Result[ServerInfo]:
82
+ """Start a single server with proper error handling."""
83
+ if server_config.name in self._running_servers:
84
+ return Result(
85
+ status=OperationStatus.FAILED,
86
+ error=f"Server {server_config.name} is already running",
87
+ error_code="SERVER_ALREADY_RUNNING",
88
+ )
89
+
90
+ # Validate configuration
91
+ server_type = server_config.config.get("type", "unknown")
92
+ is_valid, error_msg = self.registry.validate_server_config(
93
+ server_type, server_config
94
+ )
95
+ if not is_valid:
96
+ return Result(
97
+ status=OperationStatus.FAILED,
98
+ error=f"Invalid configuration: {error_msg}",
99
+ error_code="INVALID_CONFIG",
100
+ )
101
+
102
+ try:
103
+ # Choose startup method based on transport
104
+ if server_config.transport in ["http", "streamable-http"]:
105
+ result = await self._start_server_subprocess(
106
+ server_config, background, show_logs
107
+ )
108
+ else:
109
+ result = await self._start_server_traditional(server_config, background)
110
+
111
+ if result.is_success:
112
+ logger.info(
113
+ f"Successfully started server: {server_config.name}",
114
+ server_name=server_config.name,
115
+ transport=server_config.transport,
116
+ background=background,
117
+ )
118
+
119
+ return result
120
+
121
+ except Exception as e:
122
+ error = ServerStartupError(
123
+ f"Failed to start server {server_config.name}",
124
+ server_name=server_config.name,
125
+ cause=e,
126
+ )
127
+ logger.error("Server startup failed", error=error)
128
+ return Result(
129
+ status=OperationStatus.FAILED,
130
+ error=str(error),
131
+ error_code=error.error_code,
132
+ )
133
+
134
+ async def _start_server_subprocess(
135
+ self, server_config: ServerConfig, background: bool, show_logs: bool = True
136
+ ) -> Result[ServerInfo]:
137
+ """Start a server using subprocess (for HTTP transports)."""
138
+ try:
139
+ # Determine the correct script based on server type
140
+ server_type = server_config.config.get("type", "unknown")
141
+
142
+ # Validated list of trusted server modules (security: only allow known modules)
143
+ trusted_modules = {
144
+ "mock": "lightfast_mcp.servers.mock_server",
145
+ "blender": "lightfast_mcp.servers.blender_mcp_server",
146
+ }
147
+
148
+ if server_type not in trusted_modules:
149
+ return Result(
150
+ status=OperationStatus.FAILED,
151
+ error=f"Unknown or untrusted server type for subprocess: {server_type}",
152
+ error_code="UNTRUSTED_SERVER_TYPE",
153
+ )
154
+
155
+ script_module = trusted_modules[server_type]
156
+
157
+ # Create environment with server config
158
+ import json
159
+ import os
160
+ import shutil
161
+ import subprocess
162
+ import sys
163
+
164
+ env = os.environ.copy()
165
+ env["LIGHTFAST_MCP_SERVER_CONFIG"] = json.dumps(
166
+ {
167
+ "name": server_config.name,
168
+ "description": server_config.description,
169
+ "host": server_config.host,
170
+ "port": server_config.port,
171
+ "transport": server_config.transport,
172
+ "path": server_config.path,
173
+ "config": server_config.config,
174
+ }
175
+ )
176
+
177
+ # Start the subprocess
178
+ # Security: Use full path to python executable and validated module name
179
+ python_executable = (
180
+ sys.executable or shutil.which("python3") or shutil.which("python")
181
+ )
182
+ if not python_executable:
183
+ return Result(
184
+ status=OperationStatus.FAILED,
185
+ error="Could not find Python executable",
186
+ error_code="PYTHON_NOT_FOUND",
187
+ )
188
+
189
+ # Only capture logs if background=True AND show_logs=False
190
+ capture_logs = background and not show_logs
191
+ process = subprocess.Popen(
192
+ [python_executable, "-m", script_module], # nosec B603 - using validated module and full path
193
+ env=env,
194
+ stdout=subprocess.PIPE if capture_logs else None,
195
+ stderr=subprocess.PIPE if capture_logs else None,
196
+ text=True,
197
+ )
198
+
199
+ server_process = ServerProcess(
200
+ process=process,
201
+ process_id=process.pid,
202
+ is_background=background,
203
+ config=server_config,
204
+ )
205
+
206
+ self._running_servers[server_config.name] = server_process
207
+
208
+ # Create ServerInfo
209
+ server_info = ServerInfo(
210
+ name=server_config.name,
211
+ server_type=server_type,
212
+ state=ServerState.RUNNING,
213
+ host=server_config.host,
214
+ port=server_config.port,
215
+ transport=server_config.transport,
216
+ url=f"http://{server_config.host}:{server_config.port}{server_config.path}",
217
+ pid=process.pid,
218
+ start_time=server_process.start_time,
219
+ )
220
+
221
+ return Result(status=OperationStatus.SUCCESS, data=server_info)
222
+
223
+ except Exception as e:
224
+ return Result(
225
+ status=OperationStatus.FAILED,
226
+ error=f"Failed to start server subprocess: {e}",
227
+ error_code="SUBPROCESS_START_FAILED",
228
+ )
229
+
230
+ async def _start_server_traditional(
231
+ self, server_config: ServerConfig, background: bool
232
+ ) -> Result[ServerInfo]:
233
+ """Start a server using the traditional approach (for stdio transport)."""
234
+ try:
235
+ # Create server instance
236
+ server_type = server_config.config.get("type", "unknown")
237
+ if not server_type or server_type == "unknown":
238
+ return Result(
239
+ status=OperationStatus.FAILED,
240
+ error="Server type not specified in configuration",
241
+ error_code="MISSING_SERVER_TYPE",
242
+ )
243
+ server = self.registry.create_server(server_type, server_config)
244
+
245
+ if background:
246
+ # Run in background thread with proper asyncio handling
247
+ import threading
248
+
249
+ thread = threading.Thread(
250
+ target=self._run_server_in_thread,
251
+ args=(server, server_config.name),
252
+ daemon=True,
253
+ )
254
+ thread.start()
255
+
256
+ server_process = ServerProcess(
257
+ server=server,
258
+ thread=thread,
259
+ is_background=True,
260
+ config=server_config,
261
+ )
262
+ else:
263
+ # Run in foreground (blocking)
264
+ server_process = ServerProcess(
265
+ server=server, is_background=False, config=server_config
266
+ )
267
+ # This will block
268
+ server.run()
269
+
270
+ self._running_servers[server_config.name] = server_process
271
+
272
+ # Create ServerInfo
273
+ server_info = ServerInfo(
274
+ name=server_config.name,
275
+ server_type=server_config.config.get("type", "unknown"),
276
+ state=ServerState.RUNNING,
277
+ host=server_config.host,
278
+ port=server_config.port,
279
+ transport=server_config.transport,
280
+ start_time=server_process.start_time,
281
+ )
282
+
283
+ return Result(status=OperationStatus.SUCCESS, data=server_info)
284
+
285
+ except Exception as e:
286
+ return Result(
287
+ status=OperationStatus.FAILED,
288
+ error=f"Failed to start server traditionally: {e}",
289
+ error_code="TRADITIONAL_START_FAILED",
290
+ )
291
+
292
+ def _run_server_in_thread(self, server: BaseServer, server_name: str):
293
+ """Run a server in a background thread with proper asyncio handling."""
294
+ try:
295
+ logger.info(f"Running server {server_name} in background thread")
296
+
297
+ # Create a new event loop for this thread
298
+ # This is crucial for FastMCP servers which are asyncio-based
299
+ import asyncio
300
+
301
+ loop = asyncio.new_event_loop()
302
+ asyncio.set_event_loop(loop)
303
+
304
+ try:
305
+ # Run the server in the new event loop
306
+ server.run()
307
+ finally:
308
+ # Clean up the event loop
309
+ loop.close()
310
+
311
+ except Exception as e:
312
+ logger.error(f"Error running server {server_name}", error=e)
313
+ finally:
314
+ # Clean up the server from running list when it stops
315
+ if server_name in self._running_servers:
316
+ logger.info(f"Server {server_name} stopped")
317
+ del self._running_servers[server_name]
318
+
319
+ @with_correlation_id
320
+ async def start_multiple_servers(
321
+ self,
322
+ server_configs: List[ServerConfig],
323
+ background: bool = True,
324
+ show_logs: bool = True,
325
+ ) -> Result[Dict[str, bool]]:
326
+ """Start multiple servers concurrently."""
327
+ if not server_configs:
328
+ return Result(status=OperationStatus.SUCCESS, data={})
329
+
330
+ logger.info(f"Starting {len(server_configs)} servers concurrently")
331
+
332
+ # Create startup operations
333
+ async def start_single_server(config: ServerConfig) -> bool:
334
+ result = await self.start_server(config, background, show_logs)
335
+ return result.is_success
336
+
337
+ operations = [
338
+ lambda cfg=config: start_single_server(cfg) for config in server_configs
339
+ ]
340
+ operation_names = [f"start_{config.name}" for config in server_configs]
341
+
342
+ # Execute with controlled concurrency
343
+ results = await run_concurrent_operations(
344
+ operations,
345
+ max_concurrent=self.max_concurrent_startups,
346
+ operation_names=operation_names,
347
+ )
348
+
349
+ # Build result dictionary
350
+ startup_results = {}
351
+ for config, result in zip(server_configs, results):
352
+ # If the operation succeeded, use the data (boolean result)
353
+ # If the operation failed (exception), treat as False
354
+ startup_results[config.name] = result.data if result.is_success else False
355
+
356
+ successful = sum(1 for success in startup_results.values() if success)
357
+ logger.info(
358
+ f"Server startup completed: {successful}/{len(server_configs)} successful"
359
+ )
360
+
361
+ return Result(status=OperationStatus.SUCCESS, data=startup_results)
362
+
363
+ def get_running_servers(self) -> Dict[str, ServerInfo]:
364
+ """Get information about all running servers."""
365
+ info = {}
366
+ for name, server_process in self._running_servers.items():
367
+ if server_process.server:
368
+ info[name] = server_process.server.info
369
+ elif server_process.config:
370
+ # Create ServerInfo for subprocess-based servers
371
+ server_info = ServerInfo(
372
+ name=server_process.config.name,
373
+ server_type=server_process.config.config.get("type", "unknown"),
374
+ state=ServerState.RUNNING
375
+ if self._is_process_running(server_process)
376
+ else ServerState.ERROR,
377
+ host=server_process.config.host,
378
+ port=server_process.config.port,
379
+ transport=server_process.config.transport,
380
+ start_time=server_process.start_time,
381
+ pid=server_process.process_id,
382
+ )
383
+ if server_process.config.transport in ["http", "streamable-http"]:
384
+ server_info.url = (
385
+ f"http://{server_process.config.host}:{server_process.config.port}"
386
+ f"{server_process.config.path}"
387
+ )
388
+ info[name] = server_info
389
+ return info
390
+
391
+ def _is_process_running(self, server_process: ServerProcess) -> bool:
392
+ """Check if a subprocess is still running."""
393
+ if server_process.process:
394
+ return server_process.process.poll() is None
395
+ return False
396
+
397
+ def shutdown_all(self):
398
+ """Shutdown all running servers."""
399
+ logger.info("Shutting down all servers...")
400
+
401
+ server_names = list(self._running_servers.keys())
402
+ for name in server_names:
403
+ self.stop_server(name)
404
+
405
+ self._shutdown_event.set()
406
+ logger.info("All servers shut down")
407
+
408
+ def stop_server(self, server_name: str) -> bool:
409
+ """Stop a specific server."""
410
+ if server_name not in self._running_servers:
411
+ logger.warning(f"Server {server_name} is not running")
412
+ return False
413
+
414
+ server_process = self._running_servers[server_name]
415
+
416
+ try:
417
+ # Handle subprocess
418
+ if server_process.process:
419
+ try:
420
+ server_process.process.terminate()
421
+ # Wait up to 5 seconds for graceful shutdown
422
+ server_process.process.wait(timeout=5)
423
+ except subprocess.TimeoutExpired:
424
+ logger.warning(
425
+ f"Server {server_name} didn't stop gracefully, forcing..."
426
+ )
427
+ server_process.process.kill()
428
+ except Exception as e:
429
+ logger.error(
430
+ f"Error stopping server process {server_name}", error=e
431
+ )
432
+
433
+ # Handle threaded server
434
+ elif server_process.thread and server_process.thread.is_alive():
435
+ logger.info(
436
+ f"Server {server_name} thread is still running, waiting for natural shutdown..."
437
+ )
438
+
439
+ # Remove from running servers
440
+ del self._running_servers[server_name]
441
+ logger.info(f"Stopped server: {server_name}")
442
+ return True
443
+
444
+ except Exception as e:
445
+ logger.error(f"Error stopping server {server_name}", error=e)
446
+ return False
447
+
448
+
449
+ # Global orchestrator instance
450
+ _orchestrator: Optional[ServerOrchestrator] = None
451
+
452
+
453
+ def get_orchestrator() -> ServerOrchestrator:
454
+ """Get the global server orchestrator instance."""
455
+ global _orchestrator
456
+ if _orchestrator is None:
457
+ _orchestrator = ServerOrchestrator()
458
+ return _orchestrator
459
+
460
+
461
+ def reset_orchestrator():
462
+ """Reset the global orchestrator (mainly for testing)."""
463
+ global _orchestrator
464
+ if _orchestrator is not None:
465
+ _orchestrator.shutdown_all()
466
+ _orchestrator = None
@@ -0,0 +1,187 @@
1
+ """Server registry for discovering and managing MCP servers."""
2
+
3
+ import importlib
4
+ import inspect
5
+ import pkgutil
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from lightfast_mcp.core.base_server import BaseServer, ServerConfig
10
+ from lightfast_mcp.utils.logging_utils import get_logger
11
+
12
+ logger = get_logger("ServerRegistry")
13
+
14
+
15
+ class ServerRegistry:
16
+ """Registry for discovering and managing MCP servers."""
17
+
18
+ def __init__(self):
19
+ """Initialize the server registry."""
20
+ self._server_classes: dict[str, type[BaseServer]] = {}
21
+ self._server_instances: dict[str, BaseServer] = {}
22
+
23
+ # Auto-discover servers on initialization
24
+ self.discover_servers()
25
+
26
+ def discover_servers(self):
27
+ """Automatically discover all available server classes."""
28
+ logger.info("Discovering MCP servers...")
29
+
30
+ # Discover servers in the servers package
31
+ self._discover_servers_in_package("lightfast_mcp.servers")
32
+
33
+ logger.info(
34
+ f"Found {len(self._server_classes)} server types: {list(self._server_classes.keys())}"
35
+ )
36
+
37
+ def _discover_servers_in_package(self, package_name: str):
38
+ """Discover servers in a specific package."""
39
+ try:
40
+ # Import the package
41
+ package = importlib.import_module(package_name)
42
+
43
+ # Check if package has a file path
44
+ if package.__file__ is None:
45
+ logger.debug(f"Package {package_name} has no __file__ attribute")
46
+ return
47
+
48
+ package_path = Path(package.__file__).parent
49
+
50
+ # Iterate through all modules in the package
51
+ for _finder, name, ispkg in pkgutil.iter_modules([str(package_path)]):
52
+ if ispkg:
53
+ # Recursively search subpackages
54
+ subpackage_name = f"{package_name}.{name}"
55
+ self._discover_servers_in_package(subpackage_name)
56
+ else:
57
+ # Import the module and look for server classes
58
+ try:
59
+ module_name = f"{package_name}.{name}"
60
+ module = importlib.import_module(module_name)
61
+ self._discover_servers_in_module(module)
62
+ except Exception as e:
63
+ logger.debug(f"Could not import module {module_name}: {e}")
64
+
65
+ except Exception as e:
66
+ logger.debug(f"Could not discover servers in package {package_name}: {e}")
67
+
68
+ def _discover_servers_in_module(self, module):
69
+ """Discover server classes in a module."""
70
+ for name, obj in inspect.getmembers(module):
71
+ if (
72
+ inspect.isclass(obj)
73
+ and issubclass(obj, BaseServer)
74
+ and obj is not BaseServer
75
+ and hasattr(obj, "SERVER_TYPE")
76
+ ):
77
+ server_type = obj.SERVER_TYPE
78
+ if server_type != "base": # Skip the base class
79
+ logger.debug(f"Found server class: {name} (type: {server_type})")
80
+ self._server_classes[server_type] = obj
81
+
82
+ def register_server_class(self, server_type: str, server_class: type[BaseServer]):
83
+ """Manually register a server class."""
84
+ logger.info(f"Registering server class: {server_type}")
85
+ self._server_classes[server_type] = server_class
86
+
87
+ def get_server_class(self, server_type: str) -> type[BaseServer] | None:
88
+ """Get a server class by type."""
89
+ return self._server_classes.get(server_type)
90
+
91
+ def get_available_server_types(self) -> list[str]:
92
+ """Get list of all available server types."""
93
+ return list(self._server_classes.keys())
94
+
95
+ def create_server(self, server_type: str, config: ServerConfig) -> BaseServer:
96
+ """Create a server instance from configuration."""
97
+ server_class = self.get_server_class(server_type)
98
+ if not server_class:
99
+ raise ValueError(f"Unknown server type: {server_type}")
100
+
101
+ logger.info(f"Creating server instance: {config.name} (type: {server_type})")
102
+ server = server_class.create_from_config(config)
103
+
104
+ # Store the instance
105
+ self._server_instances[config.name] = server
106
+ return server
107
+
108
+ def get_server_instance(self, name: str) -> BaseServer | None:
109
+ """Get a running server instance by name."""
110
+ return self._server_instances.get(name)
111
+
112
+ def get_all_server_instances(self) -> dict[str, BaseServer]:
113
+ """Get all server instances."""
114
+ return self._server_instances.copy()
115
+
116
+ def remove_server_instance(self, name: str) -> bool:
117
+ """Remove a server instance."""
118
+ if name in self._server_instances:
119
+ logger.info(f"Removing server instance: {name}")
120
+ del self._server_instances[name]
121
+ return True
122
+ return False
123
+
124
+ def get_server_info(self) -> dict[str, dict[str, Any]]:
125
+ """Get information about all available server types."""
126
+ info = {}
127
+ for server_type, server_class in self._server_classes.items():
128
+ info[server_type] = {
129
+ "class_name": server_class.__name__,
130
+ "version": getattr(server_class, "SERVER_VERSION", "1.0.0"),
131
+ "required_dependencies": getattr(
132
+ server_class, "REQUIRED_DEPENDENCIES", []
133
+ ),
134
+ "required_apps": getattr(server_class, "REQUIRED_APPS", []),
135
+ "description": getattr(
136
+ server_class, "__doc__", "No description available"
137
+ ),
138
+ }
139
+ return info
140
+
141
+ def validate_server_config(
142
+ self, server_type: str, config: ServerConfig
143
+ ) -> tuple[bool, str]:
144
+ """Validate a server configuration."""
145
+ server_class = self.get_server_class(server_type)
146
+ if not server_class:
147
+ return False, f"Unknown server type: {server_type}"
148
+
149
+ # Basic validation
150
+ if not config.name:
151
+ return False, "Server name is required"
152
+
153
+ if not config.description:
154
+ return False, "Server description is required"
155
+
156
+ # Check for port conflicts with existing instances
157
+ for instance in self._server_instances.values():
158
+ if (
159
+ instance.config.port == config.port
160
+ and instance.config.host == config.host
161
+ and instance.config.transport in ["http", "streamable-http"]
162
+ and config.transport in ["http", "streamable-http"]
163
+ ):
164
+ return (
165
+ False,
166
+ f"Port {config.port} on {config.host} is already in use by {instance.config.name}",
167
+ )
168
+
169
+ return True, "Configuration is valid"
170
+
171
+
172
+ # Global registry instance
173
+ _registry: ServerRegistry | None = None
174
+
175
+
176
+ def get_registry() -> ServerRegistry:
177
+ """Get the global server registry instance."""
178
+ global _registry
179
+ if _registry is None:
180
+ _registry = ServerRegistry()
181
+ return _registry
182
+
183
+
184
+ def reset_registry():
185
+ """Reset the global registry (mainly for testing)."""
186
+ global _registry
187
+ _registry = None