signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__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 (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Copyright (c) 2025 SignalWire
4
+
5
+ This file is part of the SignalWire AI Agents SDK.
6
+
7
+ Licensed under the MIT License.
8
+ See LICENSE file in the project root for full license information.
9
+ """
10
+
11
+ """
12
+ MCP Server Manager for MCP Gateway
13
+
14
+ Handles spawning, communication, and management of MCP server processes.
15
+ Implements the MCP protocol client functionality.
16
+ """
17
+
18
+ import os
19
+ import sys
20
+ import subprocess
21
+ import json
22
+ import threading
23
+ import queue
24
+ import logging
25
+ import time
26
+ import pwd
27
+ import tempfile
28
+ import shutil
29
+ import resource
30
+ import select
31
+ from typing import Dict, Any, List, Optional, Tuple
32
+ from dataclasses import dataclass
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ @dataclass
38
+ class MCPService:
39
+ """Configuration for an MCP service"""
40
+ name: str
41
+ command: List[str]
42
+ description: str
43
+ enabled: bool = True
44
+ sandbox_config: Dict[str, Any] = None
45
+
46
+ def __post_init__(self):
47
+ # Default sandbox config if not provided
48
+ if self.sandbox_config is None:
49
+ self.sandbox_config = {
50
+ 'enabled': True,
51
+ 'resource_limits': True,
52
+ 'restricted_env': True
53
+ }
54
+
55
+ def __hash__(self):
56
+ return hash(self.name)
57
+
58
+
59
+ class MCPClient:
60
+ """Client for communicating with a single MCP server process"""
61
+
62
+ def __init__(self, service: MCPService, sandbox_base_dir: str = './sandbox'):
63
+ self.service = service
64
+ self.process = None
65
+ self.request_id = 0
66
+ self.pending_requests = {}
67
+ self.response_queue = queue.Queue()
68
+ self.reader_thread = None
69
+ self.lock = threading.Lock()
70
+ self.tools = []
71
+ self.sandbox_base_dir = sandbox_base_dir
72
+ self.sandbox_dir = None
73
+ self._shutdown = threading.Event()
74
+
75
+ def _setup_sandbox_env(self) -> Tuple[Dict[str, str], Optional[str]]:
76
+ """Create environment for the MCP process based on sandbox config
77
+
78
+ Returns:
79
+ Tuple of (environment dict, working directory)
80
+ """
81
+ sandbox_config = self.service.sandbox_config
82
+
83
+ # Check if sandboxing is disabled
84
+ if not sandbox_config.get('enabled', True):
85
+ logger.info(f"Sandboxing disabled for '{self.service.name}'")
86
+ return os.environ.copy(), sandbox_config.get('working_dir', os.getcwd())
87
+
88
+ # Create a subdirectory in the sandbox base for this process
89
+ self.sandbox_dir = os.path.join(self.sandbox_base_dir, f"mcp_{self.service.name}_{os.getpid()}")
90
+ os.makedirs(self.sandbox_dir, exist_ok=True)
91
+
92
+ # Start with appropriate environment
93
+ if sandbox_config.get('restricted_env', True):
94
+ # Restricted environment
95
+ env = {
96
+ 'PATH': os.environ.get('PATH', '/usr/bin:/bin'),
97
+ 'HOME': self.sandbox_dir,
98
+ 'TMPDIR': self.sandbox_dir,
99
+ 'TEMP': self.sandbox_dir,
100
+ 'TMP': self.sandbox_dir,
101
+ 'USER': os.environ.get('USER', 'nobody'),
102
+ 'LANG': 'C.UTF-8',
103
+ }
104
+ # Copy only essential env vars if they exist
105
+ for key in ['PYTHONPATH', 'NODE_PATH', 'JAVA_HOME']:
106
+ if key in os.environ:
107
+ env[key] = os.environ[key]
108
+ else:
109
+ # Full environment with some overrides
110
+ env = os.environ.copy()
111
+ env['HOME'] = self.sandbox_dir
112
+ env['TMPDIR'] = self.sandbox_dir
113
+ env['TEMP'] = self.sandbox_dir
114
+ env['TMP'] = self.sandbox_dir
115
+
116
+ # Always remove potentially dangerous env vars
117
+ dangerous_vars = ['LD_PRELOAD', 'LD_LIBRARY_PATH', 'DYLD_INSERT_LIBRARIES']
118
+ for var in dangerous_vars:
119
+ env.pop(var, None)
120
+
121
+ # Determine working directory
122
+ working_dir = sandbox_config.get('working_dir', os.getcwd())
123
+
124
+ return env, working_dir
125
+
126
+ def _sandbox_preexec(self):
127
+ """Pre-exec function to sandbox the process"""
128
+ sandbox_config = self.service.sandbox_config
129
+
130
+ # Check if sandboxing is disabled
131
+ if not sandbox_config.get('enabled', True):
132
+ return
133
+
134
+ # Check if resource limits are enabled
135
+ if sandbox_config.get('resource_limits', True):
136
+ try:
137
+ # Set resource limits
138
+ # Limit CPU time (300 seconds)
139
+ resource.setrlimit(resource.RLIMIT_CPU, (300, 300))
140
+ # Limit memory (512MB)
141
+ resource.setrlimit(resource.RLIMIT_AS, (512 * 1024 * 1024, 512 * 1024 * 1024))
142
+ # Limit number of processes (10)
143
+ resource.setrlimit(resource.RLIMIT_NPROC, (10, 10))
144
+ # Limit file size (10MB)
145
+ resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024, 10 * 1024 * 1024))
146
+ except Exception as e:
147
+ # Log but don't fail - some systems might not support all operations
148
+ logger.warning(f"Resource limit warning: {e}")
149
+
150
+ # Drop privileges if running as root (currently disabled)
151
+ # NOTE: Disabled for now - needs more testing with MCP servers
152
+ # if os.getuid() == 0:
153
+ # nobody_uid = pwd.getpwnam('nobody').pw_uid
154
+ # os.setgroups([]) # Remove supplementary groups
155
+ # os.setgid(nobody_uid) # Set GID first
156
+ # os.setuid(nobody_uid) # Then UID
157
+
158
+ def start(self) -> bool:
159
+ """Start the MCP server process and initialize connection"""
160
+ try:
161
+ logger.info(f"Starting MCP service '{self.service.name}' with command: {self.service.command}")
162
+
163
+ # Set up environment and working directory
164
+ env, working_dir = self._setup_sandbox_env()
165
+
166
+ # Log the command we're trying to run
167
+ logger.info(f"Starting command: {' '.join(self.service.command)}")
168
+ logger.info(f"Working directory: {working_dir}")
169
+ if self.sandbox_dir:
170
+ logger.info(f"Sandbox directory: {self.sandbox_dir}")
171
+
172
+ # Check if we should use preexec function
173
+ use_preexec = self.service.sandbox_config.get('enabled', True) and sys.platform != 'win32'
174
+
175
+ self.process = subprocess.Popen(
176
+ self.service.command,
177
+ stdin=subprocess.PIPE,
178
+ stdout=subprocess.PIPE,
179
+ stderr=subprocess.PIPE,
180
+ text=True,
181
+ bufsize=1, # Line buffered
182
+ cwd=working_dir,
183
+ env=env,
184
+ preexec_fn=self._sandbox_preexec if use_preexec else None
185
+ )
186
+
187
+ # Start reader thread
188
+ self.reader_thread = threading.Thread(target=self._read_loop, daemon=True)
189
+ self.reader_thread.start()
190
+
191
+ # Initialize the MCP session
192
+ result = self._initialize()
193
+ if not result:
194
+ logger.error(f"Failed to initialize MCP service '{self.service.name}'")
195
+ self.stop()
196
+ return False
197
+
198
+ # Get available tools
199
+ self.tools = self._list_tools()
200
+ logger.info(f"MCP service '{self.service.name}' started with {len(self.tools)} tools")
201
+
202
+ return True
203
+
204
+ except Exception as e:
205
+ logger.error(f"Error starting MCP service '{self.service.name}': {e}")
206
+ return False
207
+
208
+ def stop(self):
209
+ """Stop the MCP server process and clean up sandbox"""
210
+ # Signal shutdown to reader thread
211
+ self._shutdown.set()
212
+
213
+ if self.process:
214
+ try:
215
+ # Try graceful shutdown first (with short timeout)
216
+ try:
217
+ # Send shutdown with very short timeout
218
+ with self.lock:
219
+ request_id = self.request_id
220
+ self.request_id += 1
221
+ request = {
222
+ "jsonrpc": "2.0",
223
+ "id": request_id,
224
+ "method": "shutdown",
225
+ "params": {}
226
+ }
227
+ self._send_message(request)
228
+ time.sleep(0.2) # Very brief wait
229
+ except:
230
+ pass
231
+
232
+ # Check if still running
233
+ if self.process.poll() is None:
234
+ # Terminate process
235
+ self.process.terminate()
236
+ try:
237
+ self.process.wait(timeout=1)
238
+ except subprocess.TimeoutExpired:
239
+ # Force kill if terminate didn't work
240
+ logger.warning(f"Force killing MCP service '{self.service.name}'")
241
+ self.process.kill()
242
+ self.process.wait(timeout=1)
243
+
244
+ except Exception as e:
245
+ logger.error(f"Error stopping process: {e}")
246
+ # Last resort - force kill
247
+ try:
248
+ if self.process.poll() is None:
249
+ self.process.kill()
250
+ except:
251
+ pass
252
+
253
+ self.process = None
254
+ logger.info(f"Stopped MCP service '{self.service.name}'")
255
+
256
+ # Wait for reader thread to finish (with timeout)
257
+ if self.reader_thread and self.reader_thread.is_alive():
258
+ self.reader_thread.join(timeout=1.0)
259
+ if self.reader_thread.is_alive():
260
+ logger.warning(f"Reader thread for '{self.service.name}' did not stop gracefully")
261
+
262
+ # Clean up sandbox directory
263
+ if hasattr(self, 'sandbox_dir') and self.sandbox_dir and os.path.exists(self.sandbox_dir):
264
+ try:
265
+ shutil.rmtree(self.sandbox_dir)
266
+ logger.debug(f"Cleaned up sandbox directory: {self.sandbox_dir}")
267
+ except Exception as e:
268
+ logger.warning(f"Failed to clean up sandbox directory: {e}")
269
+
270
+ def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
271
+ """Call a tool on the MCP server"""
272
+ return self.call_method("tools/call", {
273
+ "name": tool_name,
274
+ "arguments": arguments
275
+ })
276
+
277
+ def call_method(self, method: str, params: Dict[str, Any]) -> Any:
278
+ """Call an RPC method and wait for response"""
279
+ if self._shutdown.is_set():
280
+ raise RuntimeError("Client is shutting down")
281
+
282
+ with self.lock:
283
+ request_id = self.request_id
284
+ self.request_id += 1
285
+
286
+ request = {
287
+ "jsonrpc": "2.0",
288
+ "id": request_id,
289
+ "method": method,
290
+ "params": params
291
+ }
292
+
293
+ # Create future for response
294
+ future = threading.Event()
295
+ self.pending_requests[request_id] = {"event": future, "response": None}
296
+
297
+ # Send request
298
+ self._send_message(request)
299
+
300
+ # Wait for response (with timeout)
301
+ if not future.wait(timeout=30):
302
+ with self.lock:
303
+ self.pending_requests.pop(request_id, None)
304
+ raise TimeoutError(f"Timeout waiting for response to {method}")
305
+
306
+ # Get response
307
+ with self.lock:
308
+ response_data = self.pending_requests.pop(request_id)
309
+ response = response_data["response"]
310
+
311
+ if "error" in response:
312
+ raise Exception(f"MCP Error: {response['error']}")
313
+
314
+ return response.get("result")
315
+
316
+ def get_tools(self) -> List[Dict[str, Any]]:
317
+ """Get the list of available tools"""
318
+ return self.tools.copy()
319
+
320
+ def _send_message(self, message: Dict[str, Any]):
321
+ """Send a JSON-RPC message to the server"""
322
+ if not self.process or self.process.poll() is not None:
323
+ raise RuntimeError("MCP server process is not running")
324
+
325
+ json_str = json.dumps(message)
326
+ self.process.stdin.write(json_str + '\n')
327
+ self.process.stdin.flush()
328
+ logger.debug(f"Sent to '{self.service.name}': {json_str}")
329
+
330
+ def _read_loop(self):
331
+ """Background thread to read responses from the MCP server"""
332
+ while not self._shutdown.is_set() and self.process and self.process.poll() is None:
333
+ try:
334
+ # Simple blocking read - the thread will be interrupted on shutdown
335
+ line = self.process.stdout.readline()
336
+ if not line:
337
+ break
338
+
339
+ try:
340
+ message = json.loads(line.strip())
341
+ logger.debug(f"Received from '{self.service.name}': {message}")
342
+
343
+ # Handle response
344
+ if "id" in message:
345
+ request_id = message["id"]
346
+ with self.lock:
347
+ if request_id in self.pending_requests:
348
+ self.pending_requests[request_id]["response"] = message
349
+ self.pending_requests[request_id]["event"].set()
350
+ else:
351
+ # Notification - log it
352
+ logger.info(f"Notification from '{self.service.name}': {message}")
353
+
354
+ except json.JSONDecodeError as e:
355
+ logger.error(f"Invalid JSON from '{self.service.name}': {line.strip()}")
356
+
357
+ except Exception as e:
358
+ if not self._shutdown.is_set():
359
+ logger.error(f"Error in read loop for '{self.service.name}': {e}")
360
+ break
361
+
362
+ # Check if process died and capture any errors
363
+ if self.process and self.process.poll() is not None and not self._shutdown.is_set():
364
+ try:
365
+ stderr_output = self.process.stderr.read()
366
+ if stderr_output:
367
+ logger.error(f"Process '{self.service.name}' exited with code {self.process.returncode}")
368
+ logger.error(f"Stderr: {stderr_output}")
369
+ except:
370
+ pass
371
+
372
+ logger.info(f"Read loop ended for '{self.service.name}'")
373
+
374
+ def _initialize(self) -> bool:
375
+ """Initialize the MCP session"""
376
+ try:
377
+ result = self.call_method("initialize", {
378
+ "protocolVersion": "2025-03-26",
379
+ "capabilities": {},
380
+ "clientInfo": {
381
+ "name": "mcp-gateway",
382
+ "version": "1.0.0"
383
+ }
384
+ })
385
+
386
+ logger.info(f"Initialized '{self.service.name}': {result.get('serverInfo', {})}")
387
+ return True
388
+
389
+ except Exception as e:
390
+ logger.error(f"Failed to initialize '{self.service.name}': {e}")
391
+ return False
392
+
393
+ def _list_tools(self) -> List[Dict[str, Any]]:
394
+ """Get the list of available tools from the server"""
395
+ try:
396
+ result = self.call_method("tools/list", {})
397
+ return result.get("tools", [])
398
+
399
+ except Exception as e:
400
+ logger.error(f"Failed to list tools for '{self.service.name}': {e}")
401
+ return []
402
+
403
+
404
+ class MCPManager:
405
+ """Manages multiple MCP services and their lifecycles"""
406
+
407
+ def __init__(self, config: Dict[str, Any]):
408
+ self.config = config
409
+ self.services: Dict[str, MCPService] = {}
410
+ self.clients: Dict[str, MCPClient] = {}
411
+
412
+ # Get sandbox directory from config or use default
413
+ self.sandbox_base_dir = config.get('session', {}).get('sandbox_dir', './sandbox')
414
+
415
+ # Ensure sandbox directory exists
416
+ os.makedirs(self.sandbox_base_dir, exist_ok=True)
417
+ logger.info(f"Using sandbox base directory: {self.sandbox_base_dir}")
418
+
419
+ # Load services from config
420
+ self._load_services()
421
+
422
+ def _load_services(self):
423
+ """Load service definitions from configuration"""
424
+ services_config = self.config.get('services', {})
425
+
426
+ for name, service_data in services_config.items():
427
+ if not service_data.get('enabled', True):
428
+ logger.info(f"Service '{name}' is disabled, skipping")
429
+ continue
430
+
431
+ service = MCPService(
432
+ name=name,
433
+ command=service_data['command'],
434
+ description=service_data.get('description', ''),
435
+ enabled=service_data.get('enabled', True),
436
+ sandbox_config=service_data.get('sandbox', None)
437
+ )
438
+
439
+ self.services[name] = service
440
+ logger.info(f"Loaded service '{name}': {service.description}")
441
+
442
+ def get_service(self, service_name: str) -> Optional[MCPService]:
443
+ """Get a service definition by name"""
444
+ return self.services.get(service_name)
445
+
446
+ def list_services(self) -> Dict[str, Dict[str, Any]]:
447
+ """List all available services"""
448
+ result = {}
449
+
450
+ for name, service in self.services.items():
451
+ result[name] = {
452
+ 'description': service.description,
453
+ 'enabled': service.enabled,
454
+ 'command': service.command
455
+ }
456
+
457
+ return result
458
+
459
+ def create_client(self, service_name: str) -> MCPClient:
460
+ """Create a new MCP client for a service"""
461
+ service = self.services.get(service_name)
462
+ if not service:
463
+ raise ValueError(f"Unknown service: {service_name}")
464
+
465
+ if not service.enabled:
466
+ raise ValueError(f"Service '{service_name}' is disabled")
467
+
468
+ client = MCPClient(service, self.sandbox_base_dir)
469
+ if not client.start():
470
+ raise RuntimeError(f"Failed to start MCP service '{service_name}'")
471
+
472
+ # Track active client for cleanup
473
+ self.clients[f"{service_name}_{id(client)}"] = client
474
+
475
+ return client
476
+
477
+ def get_service_tools(self, service_name: str) -> List[Dict[str, Any]]:
478
+ """Get tools for a service by starting a temporary instance"""
479
+ client = None
480
+ try:
481
+ client = self.create_client(service_name)
482
+ return client.get_tools()
483
+ finally:
484
+ if client:
485
+ client.stop()
486
+
487
+ def validate_services(self) -> Dict[str, bool]:
488
+ """Validate that all services can be started"""
489
+ results = {}
490
+
491
+ for service_name in self.services:
492
+ try:
493
+ client = self.create_client(service_name)
494
+ client.stop()
495
+ results[service_name] = True
496
+ logger.info(f"Service '{service_name}' validation: OK")
497
+ except Exception as e:
498
+ results[service_name] = False
499
+ logger.error(f"Service '{service_name}' validation failed: {e}")
500
+
501
+ return results
502
+
503
+ def shutdown(self):
504
+ """Shutdown all active MCP clients"""
505
+ logger.info(f"Shutting down {len(self.clients)} active MCP clients")
506
+
507
+ # Stop all clients
508
+ for client_id, client in list(self.clients.items()):
509
+ try:
510
+ client.stop()
511
+ self.clients.pop(client_id, None)
512
+ except Exception as e:
513
+ logger.error(f"Error stopping client {client_id}: {e}")