code-puppy 0.0.134__py3-none-any.whl → 0.0.136__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.
- code_puppy/agent.py +15 -17
- code_puppy/agents/agent_manager.py +320 -9
- code_puppy/agents/base_agent.py +58 -2
- code_puppy/agents/runtime_manager.py +68 -42
- code_puppy/command_line/command_handler.py +82 -33
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/add_command.py +183 -0
- code_puppy/command_line/mcp/base.py +35 -0
- code_puppy/command_line/mcp/handler.py +133 -0
- code_puppy/command_line/mcp/help_command.py +146 -0
- code_puppy/command_line/mcp/install_command.py +176 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +126 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +92 -0
- code_puppy/command_line/mcp/search_command.py +117 -0
- code_puppy/command_line/mcp/start_all_command.py +126 -0
- code_puppy/command_line/mcp/start_command.py +98 -0
- code_puppy/command_line/mcp/status_command.py +185 -0
- code_puppy/command_line/mcp/stop_all_command.py +109 -0
- code_puppy/command_line/mcp/stop_command.py +79 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +259 -0
- code_puppy/command_line/model_picker_completion.py +21 -4
- code_puppy/command_line/prompt_toolkit_completion.py +9 -0
- code_puppy/main.py +23 -17
- code_puppy/mcp/__init__.py +42 -16
- code_puppy/mcp/async_lifecycle.py +51 -49
- code_puppy/mcp/blocking_startup.py +125 -113
- code_puppy/mcp/captured_stdio_server.py +63 -70
- code_puppy/mcp/circuit_breaker.py +63 -47
- code_puppy/mcp/config_wizard.py +169 -136
- code_puppy/mcp/dashboard.py +79 -71
- code_puppy/mcp/error_isolation.py +147 -100
- code_puppy/mcp/examples/retry_example.py +55 -42
- code_puppy/mcp/health_monitor.py +152 -141
- code_puppy/mcp/managed_server.py +100 -97
- code_puppy/mcp/manager.py +168 -156
- code_puppy/mcp/registry.py +148 -110
- code_puppy/mcp/retry_manager.py +63 -61
- code_puppy/mcp/server_registry_catalog.py +271 -225
- code_puppy/mcp/status_tracker.py +80 -80
- code_puppy/mcp/system_tools.py +47 -52
- code_puppy/messaging/message_queue.py +20 -13
- code_puppy/messaging/renderers.py +30 -15
- code_puppy/state_management.py +103 -0
- code_puppy/tui/app.py +64 -7
- code_puppy/tui/components/chat_view.py +3 -3
- code_puppy/tui/components/human_input_modal.py +12 -8
- code_puppy/tui/screens/__init__.py +2 -2
- code_puppy/tui/screens/mcp_install_wizard.py +208 -179
- code_puppy/tui/tests/test_agent_command.py +3 -3
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
- code_puppy/command_line/mcp_commands.py +0 -1789
- {code_puppy-0.0.134.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,19 +12,24 @@ import os
|
|
|
12
12
|
import tempfile
|
|
13
13
|
import threading
|
|
14
14
|
import uuid
|
|
15
|
-
from typing import Optional, Callable, List
|
|
16
15
|
from contextlib import asynccontextmanager
|
|
17
|
-
from
|
|
16
|
+
from typing import List, Optional
|
|
17
|
+
|
|
18
18
|
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
19
|
-
from
|
|
20
|
-
|
|
19
|
+
from pydantic_ai.mcp import MCPServerStdio
|
|
20
|
+
|
|
21
21
|
from code_puppy.messaging import emit_info
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class StderrFileCapture:
|
|
25
25
|
"""Captures stderr to a file and monitors it in a background thread."""
|
|
26
|
-
|
|
27
|
-
def __init__(
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
server_name: str,
|
|
30
|
+
emit_to_user: bool = True,
|
|
31
|
+
message_group: Optional[uuid.UUID] = None,
|
|
32
|
+
):
|
|
28
33
|
self.server_name = server_name
|
|
29
34
|
self.emit_to_user = emit_to_user
|
|
30
35
|
self.message_group = message_group or uuid.uuid4()
|
|
@@ -33,30 +38,32 @@ class StderrFileCapture:
|
|
|
33
38
|
self.monitor_thread = None
|
|
34
39
|
self.stop_monitoring = threading.Event()
|
|
35
40
|
self.captured_lines = []
|
|
36
|
-
|
|
41
|
+
|
|
37
42
|
def start(self):
|
|
38
43
|
"""Start capture by creating temp file and monitor thread."""
|
|
39
44
|
# Create temp file
|
|
40
|
-
self.temp_file = tempfile.NamedTemporaryFile(
|
|
45
|
+
self.temp_file = tempfile.NamedTemporaryFile(
|
|
46
|
+
mode="w+", delete=False, suffix=".err"
|
|
47
|
+
)
|
|
41
48
|
self.temp_path = self.temp_file.name
|
|
42
|
-
|
|
49
|
+
|
|
43
50
|
# Start monitoring thread
|
|
44
51
|
self.stop_monitoring.clear()
|
|
45
52
|
self.monitor_thread = threading.Thread(target=self._monitor_file)
|
|
46
53
|
self.monitor_thread.daemon = True
|
|
47
54
|
self.monitor_thread.start()
|
|
48
|
-
|
|
55
|
+
|
|
49
56
|
return self.temp_file
|
|
50
|
-
|
|
57
|
+
|
|
51
58
|
def _monitor_file(self):
|
|
52
59
|
"""Monitor the temp file for new content."""
|
|
53
60
|
if not self.temp_path:
|
|
54
61
|
return
|
|
55
|
-
|
|
62
|
+
|
|
56
63
|
last_pos = 0
|
|
57
64
|
while not self.stop_monitoring.is_set():
|
|
58
65
|
try:
|
|
59
|
-
with open(self.temp_path,
|
|
66
|
+
with open(self.temp_path, "r") as f:
|
|
60
67
|
f.seek(last_pos)
|
|
61
68
|
new_content = f.read()
|
|
62
69
|
if new_content:
|
|
@@ -67,47 +74,47 @@ class StderrFileCapture:
|
|
|
67
74
|
self.captured_lines.append(line)
|
|
68
75
|
if self.emit_to_user:
|
|
69
76
|
emit_info(
|
|
70
|
-
f"[bold white on blue] MCP {self.server_name} [/bold white on blue] {line}",
|
|
77
|
+
f"[bold white on blue] MCP {self.server_name} [/bold white on blue] {line}",
|
|
71
78
|
style="dim cyan",
|
|
72
|
-
message_group=self.message_group
|
|
79
|
+
message_group=self.message_group,
|
|
73
80
|
)
|
|
74
|
-
|
|
81
|
+
|
|
75
82
|
except Exception:
|
|
76
83
|
pass # File might not exist yet or be deleted
|
|
77
|
-
|
|
84
|
+
|
|
78
85
|
self.stop_monitoring.wait(0.1) # Check every 100ms
|
|
79
|
-
|
|
86
|
+
|
|
80
87
|
def stop(self):
|
|
81
88
|
"""Stop monitoring and clean up."""
|
|
82
89
|
self.stop_monitoring.set()
|
|
83
90
|
if self.monitor_thread:
|
|
84
91
|
self.monitor_thread.join(timeout=1)
|
|
85
|
-
|
|
92
|
+
|
|
86
93
|
if self.temp_file:
|
|
87
94
|
try:
|
|
88
95
|
self.temp_file.close()
|
|
89
|
-
except:
|
|
96
|
+
except Exception:
|
|
90
97
|
pass
|
|
91
|
-
|
|
98
|
+
|
|
92
99
|
if self.temp_path and os.path.exists(self.temp_path):
|
|
93
100
|
try:
|
|
94
101
|
# Read any remaining content
|
|
95
|
-
with open(self.temp_path,
|
|
102
|
+
with open(self.temp_path, "r") as f:
|
|
96
103
|
content = f.read()
|
|
97
104
|
for line in content.splitlines():
|
|
98
105
|
if line.strip() and line not in self.captured_lines:
|
|
99
106
|
self.captured_lines.append(line)
|
|
100
107
|
if self.emit_to_user:
|
|
101
108
|
emit_info(
|
|
102
|
-
f"[bold white on blue] MCP {self.server_name} [/bold white on blue] {line}",
|
|
109
|
+
f"[bold white on blue] MCP {self.server_name} [/bold white on blue] {line}",
|
|
103
110
|
style="dim cyan",
|
|
104
|
-
message_group=self.message_group
|
|
111
|
+
message_group=self.message_group,
|
|
105
112
|
)
|
|
106
|
-
|
|
113
|
+
|
|
107
114
|
os.unlink(self.temp_path)
|
|
108
|
-
except:
|
|
115
|
+
except Exception:
|
|
109
116
|
pass
|
|
110
|
-
|
|
117
|
+
|
|
111
118
|
def get_captured_lines(self) -> List[str]:
|
|
112
119
|
"""Get all captured lines."""
|
|
113
120
|
return self.captured_lines.copy()
|
|
@@ -117,7 +124,7 @@ class SimpleCapturedMCPServerStdio(MCPServerStdio):
|
|
|
117
124
|
"""
|
|
118
125
|
MCPServerStdio that captures stderr to a file and optionally emits to user.
|
|
119
126
|
"""
|
|
120
|
-
|
|
127
|
+
|
|
121
128
|
def __init__(
|
|
122
129
|
self,
|
|
123
130
|
command: str,
|
|
@@ -126,34 +133,36 @@ class SimpleCapturedMCPServerStdio(MCPServerStdio):
|
|
|
126
133
|
cwd=None,
|
|
127
134
|
emit_stderr: bool = True,
|
|
128
135
|
message_group: Optional[uuid.UUID] = None,
|
|
129
|
-
**kwargs
|
|
136
|
+
**kwargs,
|
|
130
137
|
):
|
|
131
138
|
super().__init__(command=command, args=args, env=env, cwd=cwd, **kwargs)
|
|
132
139
|
self.emit_stderr = emit_stderr
|
|
133
140
|
self.message_group = message_group or uuid.uuid4()
|
|
134
141
|
self._stderr_capture = None
|
|
135
|
-
|
|
142
|
+
|
|
136
143
|
@asynccontextmanager
|
|
137
144
|
async def client_streams(self):
|
|
138
145
|
"""Create streams with stderr capture."""
|
|
139
146
|
server = StdioServerParameters(
|
|
140
|
-
command=self.command,
|
|
141
|
-
args=list(self.args),
|
|
142
|
-
env=self.env,
|
|
143
|
-
cwd=self.cwd
|
|
147
|
+
command=self.command, args=list(self.args), env=self.env, cwd=self.cwd
|
|
144
148
|
)
|
|
145
|
-
|
|
149
|
+
|
|
146
150
|
# Create stderr capture
|
|
147
|
-
server_name = getattr(self,
|
|
148
|
-
self._stderr_capture = StderrFileCapture(
|
|
151
|
+
server_name = getattr(self, "tool_prefix", self.command)
|
|
152
|
+
self._stderr_capture = StderrFileCapture(
|
|
153
|
+
server_name, self.emit_stderr, self.message_group
|
|
154
|
+
)
|
|
149
155
|
stderr_file = self._stderr_capture.start()
|
|
150
|
-
|
|
156
|
+
|
|
151
157
|
try:
|
|
152
|
-
async with stdio_client(server=server, errlog=stderr_file) as (
|
|
158
|
+
async with stdio_client(server=server, errlog=stderr_file) as (
|
|
159
|
+
read_stream,
|
|
160
|
+
write_stream,
|
|
161
|
+
):
|
|
153
162
|
yield read_stream, write_stream
|
|
154
163
|
finally:
|
|
155
164
|
self._stderr_capture.stop()
|
|
156
|
-
|
|
165
|
+
|
|
157
166
|
def get_captured_stderr(self) -> List[str]:
|
|
158
167
|
"""Get captured stderr lines."""
|
|
159
168
|
if self._stderr_capture:
|
|
@@ -164,97 +173,99 @@ class SimpleCapturedMCPServerStdio(MCPServerStdio):
|
|
|
164
173
|
class BlockingMCPServerStdio(SimpleCapturedMCPServerStdio):
|
|
165
174
|
"""
|
|
166
175
|
MCP Server that blocks until fully initialized.
|
|
167
|
-
|
|
176
|
+
|
|
168
177
|
This server ensures that initialization is complete before
|
|
169
178
|
allowing any operations, preventing race conditions.
|
|
170
179
|
"""
|
|
171
|
-
|
|
180
|
+
|
|
172
181
|
def __init__(self, *args, **kwargs):
|
|
173
182
|
super().__init__(*args, **kwargs)
|
|
174
183
|
self._initialized = asyncio.Event()
|
|
175
184
|
self._init_error: Optional[Exception] = None
|
|
176
185
|
self._initialization_task = None
|
|
177
|
-
|
|
186
|
+
|
|
178
187
|
async def __aenter__(self):
|
|
179
188
|
"""Enter context and track initialization."""
|
|
180
189
|
try:
|
|
181
190
|
# Start initialization
|
|
182
191
|
result = await super().__aenter__()
|
|
183
|
-
|
|
192
|
+
|
|
184
193
|
# Mark as initialized
|
|
185
194
|
self._initialized.set()
|
|
186
|
-
|
|
195
|
+
|
|
187
196
|
# Emit success message
|
|
188
|
-
server_name = getattr(self,
|
|
197
|
+
server_name = getattr(self, "tool_prefix", self.command)
|
|
189
198
|
emit_info(
|
|
190
|
-
f"✅ MCP Server '{server_name}' initialized successfully",
|
|
199
|
+
f"✅ MCP Server '{server_name}' initialized successfully",
|
|
191
200
|
style="green",
|
|
192
|
-
message_group=self.message_group
|
|
201
|
+
message_group=self.message_group,
|
|
193
202
|
)
|
|
194
|
-
|
|
203
|
+
|
|
195
204
|
return result
|
|
196
|
-
|
|
205
|
+
|
|
197
206
|
except Exception as e:
|
|
198
207
|
# Store error and mark as initialized (with error)
|
|
199
208
|
self._init_error = e
|
|
200
209
|
self._initialized.set()
|
|
201
|
-
|
|
210
|
+
|
|
202
211
|
# Emit error message
|
|
203
|
-
server_name = getattr(self,
|
|
212
|
+
server_name = getattr(self, "tool_prefix", self.command)
|
|
204
213
|
emit_info(
|
|
205
|
-
f"❌ MCP Server '{server_name}' failed to initialize: {e}",
|
|
214
|
+
f"❌ MCP Server '{server_name}' failed to initialize: {e}",
|
|
206
215
|
style="red",
|
|
207
|
-
message_group=self.message_group
|
|
216
|
+
message_group=self.message_group,
|
|
208
217
|
)
|
|
209
|
-
|
|
218
|
+
|
|
210
219
|
raise
|
|
211
|
-
|
|
220
|
+
|
|
212
221
|
async def wait_until_ready(self, timeout: float = 30.0) -> bool:
|
|
213
222
|
"""
|
|
214
223
|
Wait until the server is ready.
|
|
215
|
-
|
|
224
|
+
|
|
216
225
|
Args:
|
|
217
226
|
timeout: Maximum time to wait in seconds
|
|
218
|
-
|
|
227
|
+
|
|
219
228
|
Returns:
|
|
220
229
|
True if server is ready, False if timeout or error
|
|
221
|
-
|
|
230
|
+
|
|
222
231
|
Raises:
|
|
223
232
|
TimeoutError: If server doesn't initialize within timeout
|
|
224
233
|
Exception: If server initialization failed
|
|
225
234
|
"""
|
|
226
235
|
try:
|
|
227
236
|
await asyncio.wait_for(self._initialized.wait(), timeout=timeout)
|
|
228
|
-
|
|
237
|
+
|
|
229
238
|
# Check if there was an initialization error
|
|
230
239
|
if self._init_error:
|
|
231
240
|
raise self._init_error
|
|
232
|
-
|
|
241
|
+
|
|
233
242
|
return True
|
|
234
|
-
|
|
243
|
+
|
|
235
244
|
except asyncio.TimeoutError:
|
|
236
|
-
server_name = getattr(self,
|
|
237
|
-
raise TimeoutError(
|
|
238
|
-
|
|
245
|
+
server_name = getattr(self, "tool_prefix", self.command)
|
|
246
|
+
raise TimeoutError(
|
|
247
|
+
f"Server '{server_name}' initialization timeout after {timeout}s"
|
|
248
|
+
)
|
|
249
|
+
|
|
239
250
|
async def ensure_ready(self, timeout: float = 30.0):
|
|
240
251
|
"""
|
|
241
252
|
Ensure server is ready before proceeding.
|
|
242
|
-
|
|
253
|
+
|
|
243
254
|
This is a convenience method that raises if not ready.
|
|
244
|
-
|
|
255
|
+
|
|
245
256
|
Args:
|
|
246
257
|
timeout: Maximum time to wait in seconds
|
|
247
|
-
|
|
258
|
+
|
|
248
259
|
Raises:
|
|
249
260
|
TimeoutError: If server doesn't initialize within timeout
|
|
250
261
|
Exception: If server initialization failed
|
|
251
262
|
"""
|
|
252
263
|
await self.wait_until_ready(timeout)
|
|
253
|
-
|
|
264
|
+
|
|
254
265
|
def is_ready(self) -> bool:
|
|
255
266
|
"""
|
|
256
267
|
Check if server is ready without blocking.
|
|
257
|
-
|
|
268
|
+
|
|
258
269
|
Returns:
|
|
259
270
|
True if server is initialized and ready
|
|
260
271
|
"""
|
|
@@ -264,33 +275,34 @@ class BlockingMCPServerStdio(SimpleCapturedMCPServerStdio):
|
|
|
264
275
|
class StartupMonitor:
|
|
265
276
|
"""
|
|
266
277
|
Monitor for tracking multiple server startups.
|
|
267
|
-
|
|
278
|
+
|
|
268
279
|
This class helps coordinate startup of multiple MCP servers
|
|
269
280
|
and ensures all are ready before proceeding.
|
|
270
281
|
"""
|
|
271
|
-
|
|
282
|
+
|
|
272
283
|
def __init__(self, message_group: Optional[uuid.UUID] = None):
|
|
273
284
|
self.servers = {}
|
|
274
285
|
self.startup_times = {}
|
|
275
286
|
self.message_group = message_group or uuid.uuid4()
|
|
276
|
-
|
|
287
|
+
|
|
277
288
|
def add_server(self, name: str, server: BlockingMCPServerStdio):
|
|
278
289
|
"""Add a server to monitor."""
|
|
279
290
|
self.servers[name] = server
|
|
280
|
-
|
|
291
|
+
|
|
281
292
|
async def wait_all_ready(self, timeout: float = 30.0) -> dict:
|
|
282
293
|
"""
|
|
283
294
|
Wait for all servers to be ready.
|
|
284
|
-
|
|
295
|
+
|
|
285
296
|
Args:
|
|
286
297
|
timeout: Maximum time to wait for all servers
|
|
287
|
-
|
|
298
|
+
|
|
288
299
|
Returns:
|
|
289
300
|
Dictionary of server names to ready status
|
|
290
301
|
"""
|
|
291
302
|
import time
|
|
303
|
+
|
|
292
304
|
results = {}
|
|
293
|
-
|
|
305
|
+
|
|
294
306
|
# Create tasks for all servers
|
|
295
307
|
async def wait_server(name: str, server: BlockingMCPServerStdio):
|
|
296
308
|
start = time.time()
|
|
@@ -299,52 +311,52 @@ class StartupMonitor:
|
|
|
299
311
|
self.startup_times[name] = time.time() - start
|
|
300
312
|
results[name] = True
|
|
301
313
|
emit_info(
|
|
302
|
-
f" {name}: Ready in {self.startup_times[name]:.2f}s",
|
|
314
|
+
f" {name}: Ready in {self.startup_times[name]:.2f}s",
|
|
303
315
|
style="dim green",
|
|
304
|
-
message_group=self.message_group
|
|
316
|
+
message_group=self.message_group,
|
|
305
317
|
)
|
|
306
318
|
except Exception as e:
|
|
307
319
|
self.startup_times[name] = time.time() - start
|
|
308
320
|
results[name] = False
|
|
309
321
|
emit_info(
|
|
310
|
-
f" {name}: Failed after {self.startup_times[name]:.2f}s - {e}",
|
|
322
|
+
f" {name}: Failed after {self.startup_times[name]:.2f}s - {e}",
|
|
311
323
|
style="dim red",
|
|
312
|
-
message_group=self.message_group
|
|
324
|
+
message_group=self.message_group,
|
|
313
325
|
)
|
|
314
|
-
|
|
326
|
+
|
|
315
327
|
# Wait for all servers in parallel
|
|
316
328
|
emit_info(
|
|
317
|
-
f"⏳ Waiting for {len(self.servers)} MCP servers to initialize...",
|
|
329
|
+
f"⏳ Waiting for {len(self.servers)} MCP servers to initialize...",
|
|
318
330
|
style="cyan",
|
|
319
|
-
message_group=self.message_group
|
|
331
|
+
message_group=self.message_group,
|
|
320
332
|
)
|
|
321
|
-
|
|
333
|
+
|
|
322
334
|
tasks = [
|
|
323
335
|
asyncio.create_task(wait_server(name, server))
|
|
324
336
|
for name, server in self.servers.items()
|
|
325
337
|
]
|
|
326
|
-
|
|
338
|
+
|
|
327
339
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
328
|
-
|
|
340
|
+
|
|
329
341
|
# Report summary
|
|
330
342
|
ready_count = sum(1 for r in results.values() if r)
|
|
331
343
|
total_count = len(results)
|
|
332
|
-
|
|
344
|
+
|
|
333
345
|
if ready_count == total_count:
|
|
334
346
|
emit_info(
|
|
335
|
-
f"✅ All {total_count} servers ready!",
|
|
347
|
+
f"✅ All {total_count} servers ready!",
|
|
336
348
|
style="green bold",
|
|
337
|
-
message_group=self.message_group
|
|
349
|
+
message_group=self.message_group,
|
|
338
350
|
)
|
|
339
351
|
else:
|
|
340
352
|
emit_info(
|
|
341
|
-
f"⚠️ {ready_count}/{total_count} servers ready",
|
|
353
|
+
f"⚠️ {ready_count}/{total_count} servers ready",
|
|
342
354
|
style="yellow",
|
|
343
|
-
message_group=self.message_group
|
|
355
|
+
message_group=self.message_group,
|
|
344
356
|
)
|
|
345
|
-
|
|
357
|
+
|
|
346
358
|
return results
|
|
347
|
-
|
|
359
|
+
|
|
348
360
|
def get_startup_report(self) -> str:
|
|
349
361
|
"""Get a report of startup times."""
|
|
350
362
|
lines = ["Server Startup Times:"]
|
|
@@ -354,51 +366,51 @@ class StartupMonitor:
|
|
|
354
366
|
return "\n".join(lines)
|
|
355
367
|
|
|
356
368
|
|
|
357
|
-
async def start_servers_with_blocking(
|
|
369
|
+
async def start_servers_with_blocking(
|
|
370
|
+
*servers: BlockingMCPServerStdio,
|
|
371
|
+
timeout: float = 30.0,
|
|
372
|
+
message_group: Optional[uuid.UUID] = None,
|
|
373
|
+
):
|
|
358
374
|
"""
|
|
359
375
|
Start multiple servers and wait for all to be ready.
|
|
360
|
-
|
|
376
|
+
|
|
361
377
|
Args:
|
|
362
378
|
*servers: Variable number of BlockingMCPServerStdio instances
|
|
363
379
|
timeout: Maximum time to wait for all servers
|
|
364
380
|
message_group: Optional UUID for grouping log messages
|
|
365
|
-
|
|
381
|
+
|
|
366
382
|
Returns:
|
|
367
383
|
List of ready servers
|
|
368
|
-
|
|
384
|
+
|
|
369
385
|
Example:
|
|
370
386
|
server1 = BlockingMCPServerStdio(...)
|
|
371
387
|
server2 = BlockingMCPServerStdio(...)
|
|
372
388
|
ready = await start_servers_with_blocking(server1, server2)
|
|
373
389
|
"""
|
|
374
390
|
monitor = StartupMonitor(message_group=message_group)
|
|
375
|
-
|
|
391
|
+
|
|
376
392
|
for i, server in enumerate(servers):
|
|
377
|
-
name = getattr(server,
|
|
393
|
+
name = getattr(server, "tool_prefix", f"server-{i}")
|
|
378
394
|
monitor.add_server(name, server)
|
|
379
|
-
|
|
395
|
+
|
|
380
396
|
# Start all servers
|
|
381
397
|
async def start_server(server):
|
|
382
398
|
async with server:
|
|
383
399
|
await asyncio.sleep(0.1) # Keep context alive briefly
|
|
384
400
|
return server
|
|
385
|
-
|
|
401
|
+
|
|
386
402
|
# Start servers in parallel
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
for server in servers
|
|
390
|
-
]
|
|
391
|
-
|
|
403
|
+
[asyncio.create_task(start_server(server)) for server in servers]
|
|
404
|
+
|
|
392
405
|
# Wait for all to be ready
|
|
393
406
|
results = await monitor.wait_all_ready(timeout)
|
|
394
|
-
|
|
407
|
+
|
|
395
408
|
# Get the report
|
|
396
409
|
emit_info(monitor.get_startup_report(), message_group=monitor.message_group)
|
|
397
|
-
|
|
410
|
+
|
|
398
411
|
# Return ready servers
|
|
399
412
|
ready_servers = [
|
|
400
|
-
server for name, server in monitor.servers.items()
|
|
401
|
-
if results.get(name, False)
|
|
413
|
+
server for name, server in monitor.servers.items() if results.get(name, False)
|
|
402
414
|
]
|
|
403
|
-
|
|
404
|
-
return ready_servers
|
|
415
|
+
|
|
416
|
+
return ready_servers
|