ida-pro-mcp-xjoker 1.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 (45) hide show
  1. ida_pro_mcp/__init__.py +0 -0
  2. ida_pro_mcp/__main__.py +6 -0
  3. ida_pro_mcp/ida_mcp/__init__.py +68 -0
  4. ida_pro_mcp/ida_mcp/api_analysis.py +1296 -0
  5. ida_pro_mcp/ida_mcp/api_core.py +337 -0
  6. ida_pro_mcp/ida_mcp/api_debug.py +617 -0
  7. ida_pro_mcp/ida_mcp/api_memory.py +304 -0
  8. ida_pro_mcp/ida_mcp/api_modify.py +406 -0
  9. ida_pro_mcp/ida_mcp/api_python.py +179 -0
  10. ida_pro_mcp/ida_mcp/api_resources.py +295 -0
  11. ida_pro_mcp/ida_mcp/api_stack.py +167 -0
  12. ida_pro_mcp/ida_mcp/api_types.py +480 -0
  13. ida_pro_mcp/ida_mcp/auth.py +166 -0
  14. ida_pro_mcp/ida_mcp/cache.py +232 -0
  15. ida_pro_mcp/ida_mcp/config.py +228 -0
  16. ida_pro_mcp/ida_mcp/framework.py +547 -0
  17. ida_pro_mcp/ida_mcp/http.py +859 -0
  18. ida_pro_mcp/ida_mcp/port_utils.py +104 -0
  19. ida_pro_mcp/ida_mcp/rpc.py +187 -0
  20. ida_pro_mcp/ida_mcp/server_manager.py +339 -0
  21. ida_pro_mcp/ida_mcp/sync.py +233 -0
  22. ida_pro_mcp/ida_mcp/tests/__init__.py +14 -0
  23. ida_pro_mcp/ida_mcp/tests/test_api_analysis.py +336 -0
  24. ida_pro_mcp/ida_mcp/tests/test_api_core.py +237 -0
  25. ida_pro_mcp/ida_mcp/tests/test_api_memory.py +207 -0
  26. ida_pro_mcp/ida_mcp/tests/test_api_modify.py +123 -0
  27. ida_pro_mcp/ida_mcp/tests/test_api_resources.py +199 -0
  28. ida_pro_mcp/ida_mcp/tests/test_api_stack.py +77 -0
  29. ida_pro_mcp/ida_mcp/tests/test_api_types.py +249 -0
  30. ida_pro_mcp/ida_mcp/ui.py +357 -0
  31. ida_pro_mcp/ida_mcp/utils.py +1186 -0
  32. ida_pro_mcp/ida_mcp/zeromcp/__init__.py +5 -0
  33. ida_pro_mcp/ida_mcp/zeromcp/jsonrpc.py +384 -0
  34. ida_pro_mcp/ida_mcp/zeromcp/mcp.py +883 -0
  35. ida_pro_mcp/ida_mcp.py +186 -0
  36. ida_pro_mcp/idalib_server.py +354 -0
  37. ida_pro_mcp/idalib_session_manager.py +259 -0
  38. ida_pro_mcp/server.py +1060 -0
  39. ida_pro_mcp/test.py +170 -0
  40. ida_pro_mcp_xjoker-1.0.1.dist-info/METADATA +405 -0
  41. ida_pro_mcp_xjoker-1.0.1.dist-info/RECORD +45 -0
  42. ida_pro_mcp_xjoker-1.0.1.dist-info/WHEEL +5 -0
  43. ida_pro_mcp_xjoker-1.0.1.dist-info/entry_points.txt +4 -0
  44. ida_pro_mcp_xjoker-1.0.1.dist-info/licenses/LICENSE +21 -0
  45. ida_pro_mcp_xjoker-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,104 @@
1
+ """端口冲突自动递增工具
2
+
3
+ 多个 IDA Pro 实例同时加载 MCP 插件时,提供端口自动递增重试功能。
4
+ """
5
+
6
+ import errno
7
+ import logging
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from .zeromcp.mcp import McpServer
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # 端口已占用的 errno 值(跨平台)
16
+ # macOS: 48, Linux: 98, Windows: 10048
17
+ _ADDR_IN_USE_ERRNOS = {errno.EADDRINUSE, 48, 98, 10048}
18
+
19
+ DEFAULT_MAX_RETRIES = 10
20
+
21
+
22
+ def try_serve_with_port_retry(
23
+ mcp_server: "McpServer",
24
+ host: str,
25
+ base_port: int,
26
+ *,
27
+ max_retries: int = DEFAULT_MAX_RETRIES,
28
+ request_handler=None,
29
+ ) -> tuple[int, list[int]]:
30
+ """尝试启动 MCP 服务器,端口冲突时自动递增重试。
31
+
32
+ Args:
33
+ mcp_server: McpServer 实例
34
+ host: 绑定地址
35
+ base_port: 起始端口
36
+ max_retries: 最大重试次数(包含首次尝试共 max_retries 次)
37
+ request_handler: HTTP 请求处理器类
38
+
39
+ Returns:
40
+ (actual_port, failed_ports) 元组:
41
+ - actual_port: 实际绑定成功的端口
42
+ - failed_ports: 绑定失败的端口列表(为空表示首次即成功)
43
+
44
+ Raises:
45
+ OSError: 所有端口尝试均失败,或遇到非端口冲突的错误
46
+ """
47
+ kwargs = {}
48
+ if request_handler is not None:
49
+ kwargs["request_handler"] = request_handler
50
+
51
+ failed_ports: list[int] = []
52
+ last_error: OSError | None = None
53
+
54
+ for i in range(max_retries):
55
+ port = base_port + i
56
+ if port > 65535:
57
+ break
58
+ try:
59
+ mcp_server.serve(host, port, **kwargs)
60
+ return port, failed_ports
61
+ except OSError as e:
62
+ if e.errno in _ADDR_IN_USE_ERRNOS:
63
+ logger.debug(f"Port {port} in use, trying next")
64
+ failed_ports.append(port)
65
+ last_error = e
66
+ else:
67
+ # 非端口冲突错误,立即抛出
68
+ raise
69
+
70
+ # 所有端口都失败了
71
+ assert last_error is not None
72
+ raise last_error
73
+
74
+
75
+ def format_port_exhausted_message(
76
+ host: str, base_port: int, failed_ports: list[int]
77
+ ) -> str:
78
+ """生成端口耗尽时的用户友好提示信息。
79
+
80
+ Args:
81
+ host: 绑定地址
82
+ base_port: 起始端口
83
+ failed_ports: 尝试失败的端口列表
84
+
85
+ Returns:
86
+ 格式化的错误提示字符串
87
+ """
88
+ if not failed_ports:
89
+ return f"[MCP] Error: Could not bind to port on {host}"
90
+
91
+ first = failed_ports[0]
92
+ last = failed_ports[-1]
93
+ ports_str = ", ".join(str(p) for p in failed_ports)
94
+
95
+ return (
96
+ f"[MCP] Error: Could not find an available port.\n"
97
+ f" Tried ports: {ports_str}\n"
98
+ f" All ports in range {first}-{last} are in use.\n"
99
+ f"\n"
100
+ f" To manually set a different port, run in IDA Python console:\n"
101
+ f" from ida_mcp.http import set_server_config\n"
102
+ f" set_server_config({{'host': '{host}', 'port': 15000}})\n"
103
+ f" Then toggle: Edit -> Plugins -> MCP Server"
104
+ )
@@ -0,0 +1,187 @@
1
+ import json
2
+ import os
3
+ from typing import Any, Optional
4
+ from .zeromcp import McpRpcRegistry, McpServer, McpToolError, McpHttpRequestHandler
5
+
6
+ MCP_UNSAFE: set[str] = set()
7
+ MCP_EXTENSIONS: dict[str, set[str]] = {} # group -> set of function names
8
+ MCP_SERVER = McpServer("ida-pro-mcp", extensions=MCP_EXTENSIONS)
9
+
10
+ # ============================================================================
11
+ # Output Size Limiting
12
+ # ============================================================================
13
+
14
+ OUTPUT_LIMIT_MAX_CHARS = 50000
15
+ OUTPUT_CACHE_MAX_SIZE = 100
16
+ _output_cache: dict[str, Any] = {}
17
+ _download_base_url: str = os.environ.get("IDA_MCP_URL", "http://127.0.0.1:13337")
18
+
19
+
20
+ def set_download_base_url(url: str) -> None:
21
+ global _download_base_url
22
+ _download_base_url = url.rstrip("/")
23
+
24
+
25
+ def get_download_base_url() -> str:
26
+ return _download_base_url
27
+
28
+
29
+ def _generate_output_id() -> str:
30
+ import uuid
31
+
32
+ return str(uuid.uuid4())
33
+
34
+
35
+ OUTPUT_LIMIT_PREVIEW_ITEMS = 10
36
+ OUTPUT_LIMIT_PREVIEW_STR_LEN = 1000
37
+
38
+
39
+ def _truncate_value(value: Any, depth: int = 0) -> Any:
40
+ if depth > 5:
41
+ return value
42
+
43
+ if isinstance(value, str) and len(value) > OUTPUT_LIMIT_PREVIEW_STR_LEN:
44
+ return value[:OUTPUT_LIMIT_PREVIEW_STR_LEN] + f"... [{len(value)} chars total]"
45
+
46
+ if isinstance(value, list):
47
+ truncated_list = [
48
+ _truncate_value(item, depth + 1)
49
+ for item in value[:OUTPUT_LIMIT_PREVIEW_ITEMS]
50
+ ]
51
+ if len(value) > OUTPUT_LIMIT_PREVIEW_ITEMS:
52
+ truncated_list.append(
53
+ {
54
+ "_truncated": f"... and {len(value) - OUTPUT_LIMIT_PREVIEW_ITEMS} more items"
55
+ }
56
+ )
57
+ return truncated_list
58
+
59
+ if isinstance(value, dict):
60
+ return {k: _truncate_value(v, depth + 1) for k, v in value.items()}
61
+
62
+ return value
63
+
64
+
65
+ def _add_download_info(result: Any, output_id: str, total_chars: int) -> Any:
66
+ download_url = f"{_download_base_url}/output/{output_id}.json"
67
+ info = {
68
+ "_output_truncated": True,
69
+ "_total_chars": total_chars,
70
+ "_output_id": output_id,
71
+ "_download_url": download_url,
72
+ "_download_hint": f"Output truncated. Run: curl -o .ida-mcp/{output_id}.json {download_url}",
73
+ }
74
+
75
+ if isinstance(result, dict):
76
+ return {**result, **info}
77
+
78
+ if isinstance(result, list) and result:
79
+ result = list(result)
80
+ if isinstance(result[0], dict):
81
+ result[0] = {**result[0], **info}
82
+ else:
83
+ result.insert(0, info)
84
+ return result
85
+
86
+ return {"_preview": result, **info}
87
+
88
+
89
+ def get_cached_output(output_id: str) -> Optional[Any]:
90
+ return _output_cache.get(output_id)
91
+
92
+
93
+ def _cache_output(output_id: str, data: Any) -> None:
94
+ if len(_output_cache) >= OUTPUT_CACHE_MAX_SIZE:
95
+ oldest_key = next(iter(_output_cache))
96
+ del _output_cache[oldest_key]
97
+ _output_cache[output_id] = data
98
+
99
+
100
+ def _install_tools_call_patch() -> None:
101
+ original = MCP_SERVER.registry.methods["tools/call"]
102
+
103
+ def patched(
104
+ name: str, arguments: Optional[dict] = None, _meta: Optional[dict] = None
105
+ ) -> dict:
106
+ response = original(name, arguments, _meta)
107
+
108
+ if response.get("isError"):
109
+ return response
110
+
111
+ structured = response.get("structuredContent")
112
+ if structured is None:
113
+ return response
114
+
115
+ serialized = json.dumps(structured)
116
+ if len(serialized) <= OUTPUT_LIMIT_MAX_CHARS:
117
+ return response
118
+
119
+ output_id = _generate_output_id()
120
+ _cache_output(output_id, structured)
121
+
122
+ preview = _truncate_value(structured)
123
+ preview = _add_download_info(preview, output_id, len(serialized))
124
+
125
+ return {
126
+ "structuredContent": preview,
127
+ "content": response.get("content", []),
128
+ "isError": False,
129
+ }
130
+
131
+ MCP_SERVER.registry.methods["tools/call"] = patched
132
+
133
+
134
+ # Install the output limiting patch
135
+ _install_tools_call_patch()
136
+
137
+
138
+ # ============================================================================
139
+ # Decorators
140
+ # ============================================================================
141
+
142
+
143
+ def tool(func):
144
+ return MCP_SERVER.tool(func)
145
+
146
+
147
+ def resource(uri):
148
+ return MCP_SERVER.resource(uri)
149
+
150
+
151
+ def unsafe(func):
152
+ MCP_UNSAFE.add(func.__name__)
153
+ return func
154
+
155
+
156
+ def ext(group: str):
157
+ """Mark a tool as belonging to an extension group.
158
+
159
+ Tools in extension groups are hidden by default. Enable via ?ext=group query param.
160
+ Example: @ext("dbg") marks debugger tools that require ?ext=dbg to be visible.
161
+ """
162
+
163
+ def decorator(func):
164
+ if group not in MCP_EXTENSIONS:
165
+ MCP_EXTENSIONS[group] = set()
166
+ MCP_EXTENSIONS[group].add(func.__name__)
167
+ return func
168
+
169
+ return decorator
170
+
171
+
172
+ __all__ = [
173
+ "McpRpcRegistry",
174
+ "McpServer",
175
+ "McpToolError",
176
+ "McpHttpRequestHandler",
177
+ "MCP_SERVER",
178
+ "MCP_UNSAFE",
179
+ "MCP_EXTENSIONS",
180
+ "tool",
181
+ "unsafe",
182
+ "ext",
183
+ "resource",
184
+ "get_cached_output",
185
+ "set_download_base_url",
186
+ "get_download_base_url",
187
+ ]
@@ -0,0 +1,339 @@
1
+ """IDA MCP Server Manager
2
+
3
+ Manages multiple MCP server instances with support for:
4
+ - Starting/stopping individual servers
5
+ - Status monitoring
6
+ - Configuration-based server management
7
+ """
8
+
9
+ import logging
10
+ import threading
11
+ from dataclasses import dataclass, field
12
+ from typing import Optional, Callable, TYPE_CHECKING
13
+
14
+ from .config import ServerInstanceConfig, McpConfig, get_config, save_config
15
+ from .auth import AuthMiddleware
16
+ from .port_utils import try_serve_with_port_retry
17
+
18
+ if TYPE_CHECKING:
19
+ from .zeromcp.mcp import McpServer
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class ServerInstance:
26
+ """Represents a running server instance"""
27
+
28
+ config: ServerInstanceConfig
29
+ server: Optional["McpServer"] = None
30
+ auth: AuthMiddleware = field(default_factory=AuthMiddleware)
31
+ error: Optional[str] = None
32
+ _actual_port: Optional[int] = field(default=None, repr=False)
33
+
34
+ @property
35
+ def actual_port(self) -> int:
36
+ """实际绑定的端口(可能因端口冲突自动递增)。"""
37
+ if self._actual_port is not None:
38
+ return self._actual_port
39
+ return self.config.port
40
+
41
+ @property
42
+ def is_running(self) -> bool:
43
+ return self.server is not None and self.server._running
44
+
45
+ @property
46
+ def status(self) -> str:
47
+ if self.error:
48
+ return f"error: {self.error}"
49
+ if self.is_running:
50
+ return "running"
51
+ return "stopped"
52
+
53
+ def to_status_dict(self) -> dict:
54
+ result = {
55
+ "instance_id": self.config.instance_id,
56
+ "host": self.config.host,
57
+ "port": self.config.port,
58
+ "status": self.status,
59
+ "auth_enabled": self.config.auth_enabled,
60
+ "address": self.config.address,
61
+ }
62
+ if self._actual_port is not None and self._actual_port != self.config.port:
63
+ result["actual_port"] = self._actual_port
64
+ result["address"] = f"{self.config.host}:{self._actual_port}"
65
+ return result
66
+
67
+
68
+ class ServerManager:
69
+ """Manages multiple MCP server instances.
70
+
71
+ Thread-safe manager for starting, stopping, and monitoring
72
+ multiple MCP server instances.
73
+ """
74
+
75
+ def __init__(self, create_server_func: Optional[Callable[[], "McpServer"]] = None):
76
+ """Initialize the server manager.
77
+
78
+ Args:
79
+ create_server_func: Factory function to create new McpServer instances.
80
+ If None, uses default McpServer constructor.
81
+ """
82
+ self._instances: dict[str, ServerInstance] = {}
83
+ self._lock = threading.RLock()
84
+ self._create_server = create_server_func
85
+ self._request_handler_class: Optional[type] = None
86
+
87
+ def set_server_factory(self, factory: Callable[[], "McpServer"]) -> None:
88
+ """Set the server factory function."""
89
+ self._create_server = factory
90
+
91
+ def set_request_handler(self, handler_class: type) -> None:
92
+ """Set the request handler class for new servers."""
93
+ self._request_handler_class = handler_class
94
+
95
+ def _create_server_instance(self, config: ServerInstanceConfig) -> ServerInstance:
96
+ """Create a new server instance from configuration."""
97
+ instance = ServerInstance(
98
+ config=config,
99
+ auth=AuthMiddleware(config.api_key, config.auth_enabled),
100
+ )
101
+ return instance
102
+
103
+ def add_server(self, config: ServerInstanceConfig) -> str:
104
+ """Add a new server configuration.
105
+
106
+ Args:
107
+ config: Server instance configuration
108
+
109
+ Returns:
110
+ The instance ID
111
+
112
+ Raises:
113
+ ValueError: If instance ID already exists
114
+ """
115
+ with self._lock:
116
+ if config.instance_id in self._instances:
117
+ raise ValueError(f"Server instance '{config.instance_id}' already exists")
118
+
119
+ instance = self._create_server_instance(config)
120
+ self._instances[config.instance_id] = instance
121
+ return config.instance_id
122
+
123
+ def remove_server(self, instance_id: str) -> bool:
124
+ """Remove a server instance (stops it first if running).
125
+
126
+ Args:
127
+ instance_id: The instance ID to remove
128
+
129
+ Returns:
130
+ True if removed, False if not found
131
+ """
132
+ with self._lock:
133
+ if instance_id not in self._instances:
134
+ return False
135
+
136
+ # Stop if running
137
+ self.stop_server(instance_id)
138
+
139
+ del self._instances[instance_id]
140
+ return True
141
+
142
+ def start_server(self, instance_id: str) -> bool:
143
+ """Start a server instance.
144
+
145
+ Args:
146
+ instance_id: The instance ID to start
147
+
148
+ Returns:
149
+ True if started successfully, False otherwise
150
+ """
151
+ with self._lock:
152
+ instance = self._instances.get(instance_id)
153
+ if not instance:
154
+ logger.error(f"Server instance '{instance_id}' not found")
155
+ return False
156
+
157
+ if instance.is_running:
158
+ logger.info(f"Server '{instance_id}' is already running")
159
+ return True
160
+
161
+ if not self._create_server:
162
+ logger.error("No server factory configured")
163
+ return False
164
+
165
+ try:
166
+ # Create new server
167
+ server = self._create_server()
168
+
169
+ # Configure authentication
170
+ if hasattr(server, '_auth'):
171
+ server._auth = instance.auth
172
+
173
+ # Start the server with port retry
174
+ kwargs = {}
175
+ if self._request_handler_class:
176
+ kwargs["request_handler"] = self._request_handler_class
177
+
178
+ actual_port, failed_ports = try_serve_with_port_retry(
179
+ server,
180
+ instance.config.host,
181
+ instance.config.port,
182
+ **kwargs,
183
+ )
184
+
185
+ instance.server = server
186
+ instance.error = None
187
+ instance._actual_port = actual_port if actual_port != instance.config.port else None
188
+
189
+ if failed_ports:
190
+ logger.info(
191
+ f"Server '{instance_id}': port {instance.config.port} in use, "
192
+ f"auto-selected port {actual_port}"
193
+ )
194
+ logger.info(
195
+ f"Started server '{instance_id}' on {instance.config.host}:{actual_port}"
196
+ )
197
+ return True
198
+
199
+ except OSError as e:
200
+ if e.errno in (48, 98, 10048): # Address already in use
201
+ instance.error = (
202
+ f"All ports {instance.config.port}-{instance.config.port + 9} are in use"
203
+ )
204
+ else:
205
+ instance.error = str(e)
206
+ logger.error(f"Failed to start server '{instance_id}': {instance.error}")
207
+ return False
208
+ except Exception as e:
209
+ instance.error = str(e)
210
+ logger.error(f"Failed to start server '{instance_id}': {e}")
211
+ return False
212
+
213
+ def stop_server(self, instance_id: str) -> bool:
214
+ """Stop a server instance.
215
+
216
+ Args:
217
+ instance_id: The instance ID to stop
218
+
219
+ Returns:
220
+ True if stopped, False if not found or not running
221
+ """
222
+ with self._lock:
223
+ instance = self._instances.get(instance_id)
224
+ if not instance:
225
+ return False
226
+
227
+ if not instance.is_running:
228
+ return True
229
+
230
+ try:
231
+ instance.server.stop()
232
+ instance.server = None
233
+ instance.error = None
234
+ logger.info(f"Stopped server '{instance_id}'")
235
+ return True
236
+ except Exception as e:
237
+ instance.error = str(e)
238
+ logger.error(f"Error stopping server '{instance_id}': {e}")
239
+ return False
240
+
241
+ def restart_server(self, instance_id: str) -> bool:
242
+ """Restart a server instance.
243
+
244
+ Args:
245
+ instance_id: The instance ID to restart
246
+
247
+ Returns:
248
+ True if restarted successfully
249
+ """
250
+ self.stop_server(instance_id)
251
+ return self.start_server(instance_id)
252
+
253
+ def get_status(self) -> dict[str, dict]:
254
+ """Get status of all server instances.
255
+
256
+ Returns:
257
+ Dictionary mapping instance IDs to their status
258
+ """
259
+ with self._lock:
260
+ return {
261
+ instance_id: instance.to_status_dict()
262
+ for instance_id, instance in self._instances.items()
263
+ }
264
+
265
+ def get_instance(self, instance_id: str) -> Optional[ServerInstance]:
266
+ """Get a server instance by ID.
267
+
268
+ Args:
269
+ instance_id: The instance ID
270
+
271
+ Returns:
272
+ The server instance or None
273
+ """
274
+ with self._lock:
275
+ return self._instances.get(instance_id)
276
+
277
+ def load_from_config(self, config: Optional[McpConfig] = None) -> None:
278
+ """Load server instances from configuration.
279
+
280
+ Args:
281
+ config: Configuration to load from (uses global config if None)
282
+ """
283
+ if config is None:
284
+ config = get_config()
285
+
286
+ with self._lock:
287
+ # Stop and remove existing instances
288
+ for instance_id in list(self._instances.keys()):
289
+ self.remove_server(instance_id)
290
+
291
+ # Add instances from config
292
+ for server_config in config.servers:
293
+ if server_config.enabled:
294
+ self.add_server(server_config)
295
+
296
+ def start_auto_servers(self) -> int:
297
+ """Start all servers marked with auto_start.
298
+
299
+ Returns:
300
+ Number of servers started
301
+ """
302
+ count = 0
303
+ with self._lock:
304
+ for instance_id, instance in self._instances.items():
305
+ if instance.config.auto_start and not instance.is_running:
306
+ if self.start_server(instance_id):
307
+ count += 1
308
+ return count
309
+
310
+ def stop_all(self) -> None:
311
+ """Stop all running server instances."""
312
+ with self._lock:
313
+ for instance_id in list(self._instances.keys()):
314
+ self.stop_server(instance_id)
315
+
316
+ def __len__(self) -> int:
317
+ return len(self._instances)
318
+
319
+ def __contains__(self, instance_id: str) -> bool:
320
+ return instance_id in self._instances
321
+
322
+
323
+ # Global server manager instance
324
+ _manager: Optional[ServerManager] = None
325
+
326
+
327
+ def get_server_manager() -> ServerManager:
328
+ """Get the global server manager instance."""
329
+ global _manager
330
+ if _manager is None:
331
+ _manager = ServerManager()
332
+ return _manager
333
+
334
+
335
+ __all__ = [
336
+ "ServerInstance",
337
+ "ServerManager",
338
+ "get_server_manager",
339
+ ]