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
code_puppy/mcp/managed_server.py
CHANGED
|
@@ -5,22 +5,27 @@ This module provides a managed wrapper around pydantic-ai MCP server classes
|
|
|
5
5
|
that adds management capabilities while maintaining 100% compatibility.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import asyncio
|
|
9
8
|
import json
|
|
10
9
|
import logging
|
|
11
10
|
import uuid
|
|
12
11
|
from dataclasses import dataclass, field
|
|
13
12
|
from datetime import datetime, timedelta
|
|
14
13
|
from enum import Enum
|
|
15
|
-
from typing import
|
|
14
|
+
from typing import Any, Dict, Optional, Union
|
|
15
|
+
|
|
16
16
|
import httpx
|
|
17
17
|
from pydantic_ai import RunContext
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
from pydantic_ai.mcp import (
|
|
19
|
+
CallToolFunc,
|
|
20
|
+
MCPServerSSE,
|
|
21
|
+
MCPServerStdio,
|
|
22
|
+
MCPServerStreamableHTTP,
|
|
23
|
+
ToolResult,
|
|
24
|
+
)
|
|
20
25
|
|
|
21
26
|
from code_puppy.http_utils import create_async_client
|
|
22
|
-
from code_puppy.messaging import emit_info
|
|
23
27
|
from code_puppy.mcp.blocking_startup import BlockingMCPServerStdio
|
|
28
|
+
from code_puppy.messaging import emit_info
|
|
24
29
|
|
|
25
30
|
# Configure logging
|
|
26
31
|
logger = logging.getLogger(__name__)
|
|
@@ -28,6 +33,7 @@ logger = logging.getLogger(__name__)
|
|
|
28
33
|
|
|
29
34
|
class ServerState(Enum):
|
|
30
35
|
"""Enumeration of possible server states."""
|
|
36
|
+
|
|
31
37
|
STOPPED = "stopped"
|
|
32
38
|
STARTING = "starting"
|
|
33
39
|
RUNNING = "running"
|
|
@@ -39,6 +45,7 @@ class ServerState(Enum):
|
|
|
39
45
|
@dataclass
|
|
40
46
|
class ServerConfig:
|
|
41
47
|
"""Configuration for an MCP server."""
|
|
48
|
+
|
|
42
49
|
id: str
|
|
43
50
|
name: str
|
|
44
51
|
type: str # "sse", "stdio", or "http"
|
|
@@ -58,45 +65,41 @@ async def process_tool_call(
|
|
|
58
65
|
f"\n[bold white on purple] MCP Tool Call - {name}[/bold white on purple]",
|
|
59
66
|
message_group=group_id,
|
|
60
67
|
)
|
|
61
|
-
emit_info(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
emit_info(
|
|
66
|
-
json.dumps(tool_args, indent=2),
|
|
67
|
-
message_group=group_id
|
|
68
|
-
)
|
|
69
|
-
return await call_tool(name, tool_args, {'deps': ctx.deps})
|
|
68
|
+
emit_info("\nArgs:", message_group=group_id)
|
|
69
|
+
emit_info(json.dumps(tool_args, indent=2), message_group=group_id)
|
|
70
|
+
return await call_tool(name, tool_args, {"deps": ctx.deps})
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
class ManagedMCPServer:
|
|
73
74
|
"""
|
|
74
75
|
Managed wrapper around pydantic-ai MCP server classes.
|
|
75
|
-
|
|
76
|
+
|
|
76
77
|
This class provides management capabilities like enable/disable,
|
|
77
78
|
quarantine, and status tracking while maintaining 100% compatibility
|
|
78
79
|
with the existing Agent interface through get_pydantic_server().
|
|
79
|
-
|
|
80
|
+
|
|
80
81
|
Example usage:
|
|
81
82
|
config = ServerConfig(
|
|
82
|
-
id="123",
|
|
83
|
-
name="test",
|
|
84
|
-
type="sse",
|
|
83
|
+
id="123",
|
|
84
|
+
name="test",
|
|
85
|
+
type="sse",
|
|
85
86
|
config={"url": "http://localhost:8080"}
|
|
86
87
|
)
|
|
87
88
|
managed = ManagedMCPServer(config)
|
|
88
89
|
pydantic_server = managed.get_pydantic_server() # Returns actual MCPServerSSE
|
|
89
90
|
"""
|
|
90
|
-
|
|
91
|
+
|
|
91
92
|
def __init__(self, server_config: ServerConfig):
|
|
92
93
|
"""
|
|
93
94
|
Initialize managed server with configuration.
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
Args:
|
|
96
97
|
server_config: Server configuration containing type, connection details, etc.
|
|
97
98
|
"""
|
|
98
99
|
self.config = server_config
|
|
99
|
-
self._pydantic_server: Optional[
|
|
100
|
+
self._pydantic_server: Optional[
|
|
101
|
+
Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]
|
|
102
|
+
] = None
|
|
100
103
|
self._state = ServerState.STOPPED
|
|
101
104
|
# Always start disabled - servers must be explicitly started with /mcp start
|
|
102
105
|
self._enabled = False
|
|
@@ -104,7 +107,7 @@ class ManagedMCPServer:
|
|
|
104
107
|
self._start_time: Optional[datetime] = None
|
|
105
108
|
self._stop_time: Optional[datetime] = None
|
|
106
109
|
self._error_message: Optional[str] = None
|
|
107
|
-
|
|
110
|
+
|
|
108
111
|
# Initialize the pydantic server
|
|
109
112
|
try:
|
|
110
113
|
self._create_server()
|
|
@@ -114,52 +117,52 @@ class ManagedMCPServer:
|
|
|
114
117
|
logger.error(f"Failed to create server {self.config.name}: {e}")
|
|
115
118
|
self._state = ServerState.ERROR
|
|
116
119
|
self._error_message = str(e)
|
|
117
|
-
|
|
118
|
-
def get_pydantic_server(
|
|
120
|
+
|
|
121
|
+
def get_pydantic_server(
|
|
122
|
+
self,
|
|
123
|
+
) -> Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]:
|
|
119
124
|
"""
|
|
120
125
|
Get the actual pydantic-ai server instance.
|
|
121
|
-
|
|
126
|
+
|
|
122
127
|
This method returns the real pydantic-ai MCP server objects for 100% compatibility
|
|
123
128
|
with the existing Agent interface. Do not return custom classes or proxies.
|
|
124
|
-
|
|
129
|
+
|
|
125
130
|
Returns:
|
|
126
131
|
Actual pydantic-ai MCP server instance (MCPServerSSE, MCPServerStdio, or MCPServerStreamableHTTP)
|
|
127
|
-
|
|
132
|
+
|
|
128
133
|
Raises:
|
|
129
134
|
RuntimeError: If server creation failed or server is not available
|
|
130
135
|
"""
|
|
131
136
|
if self._pydantic_server is None:
|
|
132
137
|
raise RuntimeError(f"Server {self.config.name} is not available")
|
|
133
|
-
|
|
138
|
+
|
|
134
139
|
if not self.is_enabled() or self.is_quarantined():
|
|
135
140
|
raise RuntimeError(f"Server {self.config.name} is disabled or quarantined")
|
|
136
|
-
|
|
141
|
+
|
|
137
142
|
return self._pydantic_server
|
|
138
|
-
|
|
143
|
+
|
|
139
144
|
def _create_server(self) -> None:
|
|
140
145
|
"""
|
|
141
146
|
Create appropriate pydantic-ai server based on config type.
|
|
142
|
-
|
|
147
|
+
|
|
143
148
|
Raises:
|
|
144
149
|
ValueError: If server type is unsupported or config is invalid
|
|
145
150
|
Exception: If server creation fails
|
|
146
151
|
"""
|
|
147
152
|
server_type = self.config.type.lower()
|
|
148
153
|
config = self.config.config
|
|
149
|
-
|
|
154
|
+
|
|
150
155
|
try:
|
|
151
156
|
if server_type == "sse":
|
|
152
157
|
if "url" not in config:
|
|
153
158
|
raise ValueError("SSE server requires 'url' in config")
|
|
154
|
-
|
|
159
|
+
|
|
155
160
|
# Prepare arguments for MCPServerSSE
|
|
156
161
|
sse_kwargs = {
|
|
157
162
|
"url": config["url"],
|
|
158
163
|
}
|
|
159
|
-
|
|
164
|
+
|
|
160
165
|
# Add optional parameters if provided
|
|
161
|
-
if "headers" in config:
|
|
162
|
-
sse_kwargs["headers"] = config["headers"]
|
|
163
166
|
if "timeout" in config:
|
|
164
167
|
sse_kwargs["timeout"] = config["timeout"]
|
|
165
168
|
if "read_timeout" in config:
|
|
@@ -169,26 +172,25 @@ class ManagedMCPServer:
|
|
|
169
172
|
elif config.get("headers"):
|
|
170
173
|
# Create HTTP client if headers are provided but no client specified
|
|
171
174
|
sse_kwargs["http_client"] = self._get_http_client()
|
|
172
|
-
|
|
173
|
-
self._pydantic_server = MCPServerSSE(
|
|
174
|
-
|
|
175
|
+
|
|
176
|
+
self._pydantic_server = MCPServerSSE(
|
|
177
|
+
**sse_kwargs, process_tool_call=process_tool_call
|
|
178
|
+
)
|
|
179
|
+
|
|
175
180
|
elif server_type == "stdio":
|
|
176
181
|
if "command" not in config:
|
|
177
182
|
raise ValueError("Stdio server requires 'command' in config")
|
|
178
|
-
|
|
183
|
+
|
|
179
184
|
# Handle command and arguments
|
|
180
185
|
command = config["command"]
|
|
181
186
|
args = config.get("args", [])
|
|
182
187
|
if isinstance(args, str):
|
|
183
188
|
# If args is a string, split it
|
|
184
189
|
args = args.split()
|
|
185
|
-
|
|
190
|
+
|
|
186
191
|
# Prepare arguments for MCPServerStdio
|
|
187
|
-
stdio_kwargs = {
|
|
188
|
-
|
|
189
|
-
"args": list(args) if args else []
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
+
stdio_kwargs = {"command": command, "args": list(args) if args else []}
|
|
193
|
+
|
|
192
194
|
# Add optional parameters if provided
|
|
193
195
|
if "env" in config:
|
|
194
196
|
stdio_kwargs["env"] = config["env"]
|
|
@@ -198,30 +200,28 @@ class ManagedMCPServer:
|
|
|
198
200
|
stdio_kwargs["timeout"] = config["timeout"]
|
|
199
201
|
if "read_timeout" in config:
|
|
200
202
|
stdio_kwargs["read_timeout"] = config["read_timeout"]
|
|
201
|
-
|
|
203
|
+
|
|
202
204
|
# Use BlockingMCPServerStdio for proper initialization blocking and stderr capture
|
|
203
205
|
# Create a unique message group for this server
|
|
204
206
|
message_group = uuid.uuid4()
|
|
205
207
|
self._pydantic_server = BlockingMCPServerStdio(
|
|
206
|
-
**stdio_kwargs,
|
|
207
|
-
process_tool_call=process_tool_call,
|
|
208
|
+
**stdio_kwargs,
|
|
209
|
+
process_tool_call=process_tool_call,
|
|
208
210
|
tool_prefix=config["name"],
|
|
209
211
|
emit_stderr=True, # Always emit stderr for now
|
|
210
|
-
message_group=message_group
|
|
212
|
+
message_group=message_group,
|
|
211
213
|
)
|
|
212
|
-
|
|
214
|
+
|
|
213
215
|
elif server_type == "http":
|
|
214
216
|
if "url" not in config:
|
|
215
217
|
raise ValueError("HTTP server requires 'url' in config")
|
|
216
|
-
|
|
218
|
+
|
|
217
219
|
# Prepare arguments for MCPServerStreamableHTTP
|
|
218
220
|
http_kwargs = {
|
|
219
221
|
"url": config["url"],
|
|
220
222
|
}
|
|
221
|
-
|
|
223
|
+
|
|
222
224
|
# Add optional parameters if provided
|
|
223
|
-
if "headers" in config:
|
|
224
|
-
http_kwargs["headers"] = config["headers"]
|
|
225
225
|
if "timeout" in config:
|
|
226
226
|
http_kwargs["timeout"] = config["timeout"]
|
|
227
227
|
if "read_timeout" in config:
|
|
@@ -231,33 +231,34 @@ class ManagedMCPServer:
|
|
|
231
231
|
elif config.get("headers"):
|
|
232
232
|
# Create HTTP client if headers are provided but no client specified
|
|
233
233
|
http_kwargs["http_client"] = self._get_http_client()
|
|
234
|
-
|
|
235
|
-
self._pydantic_server = MCPServerStreamableHTTP(
|
|
236
|
-
|
|
234
|
+
|
|
235
|
+
self._pydantic_server = MCPServerStreamableHTTP(
|
|
236
|
+
**http_kwargs, process_tool_call=process_tool_call
|
|
237
|
+
)
|
|
238
|
+
|
|
237
239
|
else:
|
|
238
240
|
raise ValueError(f"Unsupported server type: {server_type}")
|
|
239
|
-
|
|
241
|
+
|
|
240
242
|
logger.info(f"Created {server_type} server: {self.config.name}")
|
|
241
|
-
|
|
243
|
+
|
|
242
244
|
except Exception as e:
|
|
243
|
-
logger.error(
|
|
245
|
+
logger.error(
|
|
246
|
+
f"Failed to create {server_type} server {self.config.name}: {e}"
|
|
247
|
+
)
|
|
244
248
|
raise
|
|
245
|
-
|
|
249
|
+
|
|
246
250
|
def _get_http_client(self) -> httpx.AsyncClient:
|
|
247
251
|
"""
|
|
248
252
|
Create httpx.AsyncClient with headers from config.
|
|
249
|
-
|
|
253
|
+
|
|
250
254
|
Returns:
|
|
251
255
|
Configured async HTTP client with custom headers
|
|
252
256
|
"""
|
|
253
257
|
headers = self.config.config.get("headers", {})
|
|
254
258
|
timeout = self.config.config.get("timeout", 30)
|
|
255
|
-
client = create_async_client(
|
|
256
|
-
headers=headers,
|
|
257
|
-
timeout=timeout
|
|
258
|
-
)
|
|
259
|
+
client = create_async_client(headers=headers, timeout=timeout)
|
|
259
260
|
return client
|
|
260
|
-
|
|
261
|
+
|
|
261
262
|
def enable(self) -> None:
|
|
262
263
|
"""Enable server availability."""
|
|
263
264
|
self._enabled = True
|
|
@@ -265,7 +266,7 @@ class ManagedMCPServer:
|
|
|
265
266
|
self._state = ServerState.RUNNING
|
|
266
267
|
self._start_time = datetime.now()
|
|
267
268
|
logger.info(f"Enabled server: {self.config.name}")
|
|
268
|
-
|
|
269
|
+
|
|
269
270
|
def disable(self) -> None:
|
|
270
271
|
"""Disable server availability."""
|
|
271
272
|
self._enabled = False
|
|
@@ -273,20 +274,20 @@ class ManagedMCPServer:
|
|
|
273
274
|
self._state = ServerState.STOPPED
|
|
274
275
|
self._stop_time = datetime.now()
|
|
275
276
|
logger.info(f"Disabled server: {self.config.name}")
|
|
276
|
-
|
|
277
|
+
|
|
277
278
|
def is_enabled(self) -> bool:
|
|
278
279
|
"""
|
|
279
280
|
Check if server is enabled.
|
|
280
|
-
|
|
281
|
+
|
|
281
282
|
Returns:
|
|
282
283
|
True if server is enabled, False otherwise
|
|
283
284
|
"""
|
|
284
285
|
return self._enabled
|
|
285
|
-
|
|
286
|
+
|
|
286
287
|
def quarantine(self, duration: int) -> None:
|
|
287
288
|
"""
|
|
288
289
|
Temporarily disable server for specified duration.
|
|
289
|
-
|
|
290
|
+
|
|
290
291
|
Args:
|
|
291
292
|
duration: Quarantine duration in seconds
|
|
292
293
|
"""
|
|
@@ -297,46 +298,48 @@ class ManagedMCPServer:
|
|
|
297
298
|
f"Quarantined server {self.config.name} for {duration} seconds "
|
|
298
299
|
f"(was {previous_state.value})"
|
|
299
300
|
)
|
|
300
|
-
|
|
301
|
+
|
|
301
302
|
def is_quarantined(self) -> bool:
|
|
302
303
|
"""
|
|
303
304
|
Check if server is currently quarantined.
|
|
304
|
-
|
|
305
|
+
|
|
305
306
|
Returns:
|
|
306
307
|
True if server is quarantined, False otherwise
|
|
307
308
|
"""
|
|
308
309
|
if self._quarantine_until is None:
|
|
309
310
|
return False
|
|
310
|
-
|
|
311
|
+
|
|
311
312
|
if datetime.now() >= self._quarantine_until:
|
|
312
313
|
# Quarantine period has expired
|
|
313
314
|
self._quarantine_until = None
|
|
314
315
|
if self._state == ServerState.QUARANTINED:
|
|
315
316
|
# Restore to running state if enabled
|
|
316
|
-
self._state =
|
|
317
|
+
self._state = (
|
|
318
|
+
ServerState.RUNNING if self._enabled else ServerState.STOPPED
|
|
319
|
+
)
|
|
317
320
|
logger.info(f"Released quarantine for server: {self.config.name}")
|
|
318
321
|
return False
|
|
319
|
-
|
|
322
|
+
|
|
320
323
|
return True
|
|
321
|
-
|
|
324
|
+
|
|
322
325
|
def get_captured_stderr(self) -> list[str]:
|
|
323
326
|
"""
|
|
324
327
|
Get captured stderr output if this is a stdio server.
|
|
325
|
-
|
|
328
|
+
|
|
326
329
|
Returns:
|
|
327
330
|
List of captured stderr lines, or empty list if not applicable
|
|
328
331
|
"""
|
|
329
332
|
if isinstance(self._pydantic_server, BlockingMCPServerStdio):
|
|
330
333
|
return self._pydantic_server.get_captured_stderr()
|
|
331
334
|
return []
|
|
332
|
-
|
|
335
|
+
|
|
333
336
|
async def wait_until_ready(self, timeout: float = 30.0) -> bool:
|
|
334
337
|
"""
|
|
335
338
|
Wait until the server is ready.
|
|
336
|
-
|
|
339
|
+
|
|
337
340
|
Args:
|
|
338
341
|
timeout: Maximum time to wait in seconds
|
|
339
|
-
|
|
342
|
+
|
|
340
343
|
Returns:
|
|
341
344
|
True if server is ready, False otherwise
|
|
342
345
|
"""
|
|
@@ -348,25 +351,25 @@ class ManagedMCPServer:
|
|
|
348
351
|
return False
|
|
349
352
|
# Non-stdio servers are considered ready immediately
|
|
350
353
|
return True
|
|
351
|
-
|
|
354
|
+
|
|
352
355
|
async def ensure_ready(self, timeout: float = 30.0):
|
|
353
356
|
"""
|
|
354
357
|
Ensure server is ready, raising exception if not.
|
|
355
|
-
|
|
358
|
+
|
|
356
359
|
Args:
|
|
357
360
|
timeout: Maximum time to wait in seconds
|
|
358
|
-
|
|
361
|
+
|
|
359
362
|
Raises:
|
|
360
363
|
TimeoutError: If server doesn't initialize within timeout
|
|
361
364
|
Exception: If server initialization failed
|
|
362
365
|
"""
|
|
363
366
|
if isinstance(self._pydantic_server, BlockingMCPServerStdio):
|
|
364
367
|
await self._pydantic_server.ensure_ready(timeout)
|
|
365
|
-
|
|
368
|
+
|
|
366
369
|
def get_status(self) -> Dict[str, Any]:
|
|
367
370
|
"""
|
|
368
371
|
Return current status information.
|
|
369
|
-
|
|
372
|
+
|
|
370
373
|
Returns:
|
|
371
374
|
Dictionary containing comprehensive status information
|
|
372
375
|
"""
|
|
@@ -374,11 +377,11 @@ class ManagedMCPServer:
|
|
|
374
377
|
uptime = None
|
|
375
378
|
if self._start_time and self._state == ServerState.RUNNING:
|
|
376
379
|
uptime = (now - self._start_time).total_seconds()
|
|
377
|
-
|
|
380
|
+
|
|
378
381
|
quarantine_remaining = None
|
|
379
382
|
if self.is_quarantined():
|
|
380
383
|
quarantine_remaining = (self._quarantine_until - now).total_seconds()
|
|
381
|
-
|
|
384
|
+
|
|
382
385
|
return {
|
|
383
386
|
"id": self.config.id,
|
|
384
387
|
"name": self.config.name,
|
|
@@ -393,9 +396,9 @@ class ManagedMCPServer:
|
|
|
393
396
|
"error_message": self._error_message,
|
|
394
397
|
"config": self.config.config.copy(), # Copy to prevent modification
|
|
395
398
|
"server_available": (
|
|
396
|
-
self._pydantic_server is not None
|
|
397
|
-
self._enabled
|
|
398
|
-
not self.is_quarantined()
|
|
399
|
-
self._state == ServerState.RUNNING
|
|
400
|
-
)
|
|
401
|
-
}
|
|
399
|
+
self._pydantic_server is not None
|
|
400
|
+
and self._enabled
|
|
401
|
+
and not self.is_quarantined()
|
|
402
|
+
and self._state == ServerState.RUNNING
|
|
403
|
+
),
|
|
404
|
+
}
|