codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,807 @@
1
+ """
2
+ MCPManager - Central coordinator for all MCP server operations.
3
+
4
+ This module provides the main MCPManager class that coordinates all MCP server
5
+ operations while maintaining pydantic-ai compatibility. It serves as the central
6
+ point for managing servers, registering configurations, and providing servers
7
+ to agents.
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+ from dataclasses import dataclass
13
+ from datetime import datetime
14
+ from typing import Any, Dict, List, Optional, Union
15
+
16
+ from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP
17
+
18
+ from .async_lifecycle import get_lifecycle_manager
19
+ from .managed_server import ManagedMCPServer, ServerConfig, ServerState
20
+ from .registry import ServerRegistry
21
+ from .status_tracker import ServerStatusTracker
22
+
23
+ # Configure logging
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass
28
+ class ServerInfo:
29
+ """Information about a registered server."""
30
+
31
+ id: str
32
+ name: str
33
+ type: str
34
+ enabled: bool
35
+ state: ServerState
36
+ quarantined: bool
37
+ uptime_seconds: Optional[float]
38
+ error_message: Optional[str]
39
+ health: Optional[Dict[str, Any]] = None
40
+ start_time: Optional[datetime] = None
41
+ latency_ms: Optional[float] = None
42
+
43
+
44
+ class MCPManager:
45
+ """
46
+ Central coordinator for all MCP server operations.
47
+
48
+ This class manages the lifecycle of MCP servers while maintaining
49
+ 100% pydantic-ai compatibility. It coordinates between the registry,
50
+ status tracker, and managed servers to provide a unified interface
51
+ for server management.
52
+
53
+ The critical method get_servers_for_agent() returns actual pydantic-ai
54
+ server instances for use with Agent objects.
55
+
56
+ Example usage:
57
+ manager = get_mcp_manager()
58
+
59
+ # Register a server
60
+ config = ServerConfig(
61
+ id="", # Auto-generated
62
+ name="filesystem",
63
+ type="stdio",
64
+ config={"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem"]}
65
+ )
66
+ server_id = manager.register_server(config)
67
+
68
+ # Get servers for agent use
69
+ servers = manager.get_servers_for_agent() # Returns actual pydantic-ai instances
70
+ """
71
+
72
+ def __init__(self):
73
+ """Initialize the MCP manager with all required components."""
74
+ # Initialize core components
75
+ self.registry = ServerRegistry()
76
+ self.status_tracker = ServerStatusTracker()
77
+
78
+ # Active managed servers (server_id -> ManagedMCPServer)
79
+ self._managed_servers: Dict[str, ManagedMCPServer] = {}
80
+
81
+ # Sync servers from mcp_servers.json into registry
82
+ self.sync_from_config()
83
+
84
+ # Load existing servers from registry
85
+ self._initialize_servers()
86
+
87
+ logger.info("MCPManager initialized with core components")
88
+
89
+ def sync_from_config(self) -> None:
90
+ """Sync servers from mcp_servers.json into the registry.
91
+
92
+ This public method ensures that servers defined in the user's
93
+ configuration file are automatically registered with the manager.
94
+ It can be called during initialization or manually to reload
95
+ server configurations.
96
+
97
+ This is the single source of truth for syncing mcp_servers.json
98
+ into the registry, avoiding duplication with base_agent.py.
99
+ """
100
+ try:
101
+ from code_puppy.config import load_mcp_server_configs
102
+
103
+ configs = load_mcp_server_configs()
104
+ if not configs:
105
+ logger.debug("No servers found in mcp_servers.json")
106
+ return
107
+
108
+ synced_count = 0
109
+ updated_count = 0
110
+
111
+ for name, conf in configs.items():
112
+ try:
113
+ # Create ServerConfig from the loaded configuration
114
+ server_config = ServerConfig(
115
+ id=conf.get("id", ""), # Empty ID will be auto-generated
116
+ name=name,
117
+ type=conf.get("type", "sse"),
118
+ enabled=conf.get("enabled", True),
119
+ config=conf,
120
+ )
121
+
122
+ # Check if server already exists by name
123
+ existing = self.registry.get_by_name(name)
124
+
125
+ if not existing:
126
+ # Register new server
127
+ self.registry.register(server_config)
128
+ synced_count += 1
129
+ logger.debug(f"Synced new server from config: {name}")
130
+ else:
131
+ # Update existing server if config has changed
132
+ if existing.config != server_config.config:
133
+ server_config.id = existing.id # Keep existing ID
134
+ self.registry.update(existing.id, server_config)
135
+ updated_count += 1
136
+ logger.debug(f"Updated server from config: {name}")
137
+
138
+ except Exception as e:
139
+ logger.warning(f"Failed to sync server '{name}' from config: {e}")
140
+ continue
141
+
142
+ if synced_count > 0 or updated_count > 0:
143
+ logger.info(
144
+ f"Synced {synced_count} new and updated {updated_count} servers from mcp_servers.json"
145
+ )
146
+
147
+ except Exception as e:
148
+ logger.error(f"Failed to sync from mcp_servers.json: {e}")
149
+ # Don't fail initialization if sync fails
150
+
151
+ def _initialize_servers(self) -> None:
152
+ """Initialize managed servers from registry configurations."""
153
+ configs = self.registry.list_all()
154
+ initialized_count = 0
155
+
156
+ for config in configs:
157
+ try:
158
+ managed_server = ManagedMCPServer(config)
159
+ self._managed_servers[config.id] = managed_server
160
+
161
+ # Update status tracker - always start as STOPPED
162
+ # Servers must be explicitly started with /mcp start
163
+ self.status_tracker.set_status(config.id, ServerState.STOPPED)
164
+
165
+ initialized_count += 1
166
+ logger.debug(
167
+ f"Initialized managed server: {config.name} (ID: {config.id})"
168
+ )
169
+
170
+ except Exception as e:
171
+ logger.error(f"Failed to initialize server {config.name}: {e}")
172
+ # Update status tracker with error state
173
+ self.status_tracker.set_status(config.id, ServerState.ERROR)
174
+ self.status_tracker.record_event(
175
+ config.id,
176
+ "initialization_error",
177
+ {"error": str(e), "message": f"Failed to initialize: {e}"},
178
+ )
179
+
180
+ logger.info(f"Initialized {initialized_count} servers from registry")
181
+
182
+ def register_server(self, config: ServerConfig) -> str:
183
+ """
184
+ Register a new server configuration.
185
+
186
+ Args:
187
+ config: Server configuration to register
188
+
189
+ Returns:
190
+ Server ID of the registered server
191
+
192
+ Raises:
193
+ ValueError: If configuration is invalid or server already exists
194
+ Exception: If server initialization fails
195
+ """
196
+ # Register with registry (validates config and assigns ID)
197
+ server_id = self.registry.register(config)
198
+
199
+ try:
200
+ # Create managed server instance
201
+ managed_server = ManagedMCPServer(config)
202
+ self._managed_servers[server_id] = managed_server
203
+
204
+ # Update status tracker - always start as STOPPED
205
+ # Servers must be explicitly started with /mcp start
206
+ self.status_tracker.set_status(server_id, ServerState.STOPPED)
207
+
208
+ # Record registration event
209
+ self.status_tracker.record_event(
210
+ server_id,
211
+ "registered",
212
+ {
213
+ "name": config.name,
214
+ "type": config.type,
215
+ "message": "Server registered successfully",
216
+ },
217
+ )
218
+
219
+ logger.info(
220
+ f"Successfully registered server: {config.name} (ID: {server_id})"
221
+ )
222
+ return server_id
223
+
224
+ except Exception as e:
225
+ # Remove from registry if initialization failed
226
+ self.registry.unregister(server_id)
227
+ logger.error(f"Failed to initialize registered server {config.name}: {e}")
228
+ raise
229
+
230
+ def get_servers_for_agent(
231
+ self,
232
+ ) -> List[Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]]:
233
+ """
234
+ Get pydantic-ai compatible servers for agent use.
235
+
236
+ This is the critical method that must return actual pydantic-ai server
237
+ instances (not wrappers). Only returns enabled, non-quarantined servers.
238
+ Handles errors gracefully by logging but not crashing.
239
+
240
+ Returns:
241
+ List of actual pydantic-ai MCP server instances ready for use
242
+ """
243
+ servers = []
244
+
245
+ for server_id, managed_server in self._managed_servers.items():
246
+ try:
247
+ # Only include enabled, non-quarantined servers
248
+ if managed_server.is_enabled() and not managed_server.is_quarantined():
249
+ # Get the actual pydantic-ai server instance
250
+ pydantic_server = managed_server.get_pydantic_server()
251
+ servers.append(pydantic_server)
252
+
253
+ logger.debug(
254
+ f"Added server to agent list: {managed_server.config.name}"
255
+ )
256
+ else:
257
+ logger.debug(
258
+ f"Skipping server {managed_server.config.name}: "
259
+ f"enabled={managed_server.is_enabled()}, "
260
+ f"quarantined={managed_server.is_quarantined()}"
261
+ )
262
+
263
+ except Exception as e:
264
+ # Log error but don't crash - continue with other servers
265
+ logger.error(
266
+ f"Error getting server {managed_server.config.name} for agent: {e}"
267
+ )
268
+ # Record error event
269
+ self.status_tracker.record_event(
270
+ server_id,
271
+ "agent_access_error",
272
+ {
273
+ "error": str(e),
274
+ "message": f"Error accessing server for agent: {e}",
275
+ },
276
+ )
277
+ continue
278
+
279
+ logger.debug(f"Returning {len(servers)} servers for agent use")
280
+ return servers
281
+
282
+ def get_server(self, server_id: str) -> Optional[ManagedMCPServer]:
283
+ """
284
+ Get managed server by ID.
285
+
286
+ Args:
287
+ server_id: ID of server to retrieve
288
+
289
+ Returns:
290
+ ManagedMCPServer instance if found, None otherwise
291
+ """
292
+ return self._managed_servers.get(server_id)
293
+
294
+ def get_server_by_name(self, name: str) -> Optional[ServerConfig]:
295
+ """
296
+ Get server configuration by name.
297
+
298
+ Args:
299
+ name: Name of server to retrieve
300
+
301
+ Returns:
302
+ ServerConfig if found, None otherwise
303
+ """
304
+ return self.registry.get_by_name(name)
305
+
306
+ def update_server(self, server_id: str, config: ServerConfig) -> bool:
307
+ """
308
+ Update server configuration.
309
+
310
+ Args:
311
+ server_id: ID of server to update
312
+ config: New configuration
313
+
314
+ Returns:
315
+ True if server was updated, False if not found
316
+ """
317
+ # Update in registry
318
+ if not self.registry.update(server_id, config):
319
+ return False
320
+
321
+ # Update managed server if it exists
322
+ managed_server = self._managed_servers.get(server_id)
323
+ if managed_server:
324
+ managed_server.config = config
325
+ # Clear cached server to force recreation on next use
326
+ managed_server.server = None
327
+ logger.info(f"Updated server configuration: {config.name}")
328
+
329
+ return True
330
+
331
+ def list_servers(self) -> List[ServerInfo]:
332
+ """
333
+ Get information about all registered servers.
334
+
335
+ Returns:
336
+ List of ServerInfo objects with current status
337
+ """
338
+ server_infos = []
339
+
340
+ for server_id, managed_server in self._managed_servers.items():
341
+ try:
342
+ status = managed_server.get_status()
343
+ uptime = self.status_tracker.get_uptime(server_id)
344
+ summary = self.status_tracker.get_server_summary(server_id)
345
+
346
+ # Get health information from metadata
347
+ health_info = self.status_tracker.get_metadata(server_id, "health")
348
+ if health_info is None:
349
+ # Create basic health info based on state
350
+ health_info = {
351
+ "is_healthy": status["state"] == "running",
352
+ "error": status.get("error_message"),
353
+ }
354
+
355
+ # Get latency from metadata
356
+ latency_ms = self.status_tracker.get_metadata(server_id, "latency_ms")
357
+
358
+ server_info = ServerInfo(
359
+ id=server_id,
360
+ name=managed_server.config.name,
361
+ type=managed_server.config.type,
362
+ enabled=managed_server.is_enabled(),
363
+ state=ServerState(status["state"]),
364
+ quarantined=managed_server.is_quarantined(),
365
+ uptime_seconds=uptime.total_seconds() if uptime else None,
366
+ error_message=status.get("error_message"),
367
+ health=health_info,
368
+ start_time=summary.get("start_time"),
369
+ latency_ms=latency_ms,
370
+ )
371
+
372
+ server_infos.append(server_info)
373
+
374
+ except Exception as e:
375
+ logger.error(f"Error getting info for server {server_id}: {e}")
376
+ # Create error info
377
+ config = self.registry.get(server_id)
378
+ if config:
379
+ server_info = ServerInfo(
380
+ id=server_id,
381
+ name=config.name,
382
+ type=config.type,
383
+ enabled=False,
384
+ state=ServerState.ERROR,
385
+ quarantined=False,
386
+ uptime_seconds=None,
387
+ error_message=str(e),
388
+ health={"is_healthy": False, "error": str(e)},
389
+ start_time=None,
390
+ latency_ms=None,
391
+ )
392
+ server_infos.append(server_info)
393
+
394
+ return server_infos
395
+
396
+ async def start_server(self, server_id: str) -> bool:
397
+ """
398
+ Start a server (enable it and start the subprocess/connection).
399
+
400
+ This both enables the server for agent use AND starts the actual process.
401
+ For stdio servers, this starts the subprocess.
402
+ For SSE/HTTP servers, this establishes the connection.
403
+
404
+ Args:
405
+ server_id: ID of server to start
406
+
407
+ Returns:
408
+ True if server was started, False if not found or failed
409
+ """
410
+ managed_server = self._managed_servers.get(server_id)
411
+ if managed_server is None:
412
+ logger.warning(f"Attempted to start non-existent server: {server_id}")
413
+ return False
414
+
415
+ try:
416
+ # First enable the server
417
+ managed_server.enable()
418
+ self.status_tracker.set_status(server_id, ServerState.RUNNING)
419
+ self.status_tracker.record_start_time(server_id)
420
+
421
+ # Try to actually start it if we have an async context
422
+ try:
423
+ # Get the pydantic-ai server instance
424
+ pydantic_server = managed_server.get_pydantic_server()
425
+
426
+ # Start the server using the async lifecycle manager
427
+ lifecycle_mgr = get_lifecycle_manager()
428
+ started = await lifecycle_mgr.start_server(server_id, pydantic_server)
429
+
430
+ if started:
431
+ logger.info(
432
+ f"Started server process: {managed_server.config.name} (ID: {server_id})"
433
+ )
434
+ self.status_tracker.record_event(
435
+ server_id,
436
+ "started",
437
+ {"message": "Server started and process running"},
438
+ )
439
+ else:
440
+ logger.warning(
441
+ f"Could not start process for server {server_id}, but it's enabled"
442
+ )
443
+ self.status_tracker.record_event(
444
+ server_id,
445
+ "enabled",
446
+ {"message": "Server enabled (process will start when used)"},
447
+ )
448
+ except Exception as e:
449
+ # Process start failed, but server is still enabled
450
+ logger.warning(f"Could not start process for server {server_id}: {e}")
451
+ self.status_tracker.record_event(
452
+ server_id,
453
+ "enabled",
454
+ {"message": "Server enabled (process will start when used)"},
455
+ )
456
+
457
+ return True
458
+
459
+ except Exception as e:
460
+ logger.error(f"Failed to start server {server_id}: {e}")
461
+ self.status_tracker.set_status(server_id, ServerState.ERROR)
462
+ self.status_tracker.record_event(
463
+ server_id,
464
+ "start_error",
465
+ {"error": str(e), "message": f"Error starting server: {e}"},
466
+ )
467
+ return False
468
+
469
+ def start_server_sync(self, server_id: str) -> bool:
470
+ """
471
+ Synchronous wrapper for start_server.
472
+
473
+ IMPORTANT: This schedules the server start as a background task.
474
+ The server subprocess will start asynchronously - it may not be
475
+ immediately ready when this function returns.
476
+ """
477
+ try:
478
+ loop = asyncio.get_running_loop()
479
+ # We're in an async context - schedule the server start as a background task
480
+ # DO NOT use blocking time.sleep() here as it freezes the event loop!
481
+
482
+ # First, enable the server immediately so it's recognized as "starting"
483
+ managed_server = self._managed_servers.get(server_id)
484
+ if managed_server:
485
+ managed_server.enable()
486
+ self.status_tracker.set_status(server_id, ServerState.STARTING)
487
+ self.status_tracker.record_start_time(server_id)
488
+
489
+ # Schedule the async start_server to run in the background
490
+ # This will properly start the subprocess and lifecycle task
491
+ async def start_server_background():
492
+ try:
493
+ result = await self.start_server(server_id)
494
+ if result:
495
+ logger.info(f"Background server start completed: {server_id}")
496
+ else:
497
+ logger.warning(f"Background server start failed: {server_id}")
498
+ return result
499
+ except Exception as e:
500
+ logger.error(f"Background server start error for {server_id}: {e}")
501
+ self.status_tracker.set_status(server_id, ServerState.ERROR)
502
+ return False
503
+
504
+ # Create the task - it will run when the event loop gets control
505
+ task = loop.create_task(
506
+ start_server_background(), name=f"start_server_{server_id}"
507
+ )
508
+
509
+ # Store task reference to prevent garbage collection
510
+ if not hasattr(self, "_pending_start_tasks"):
511
+ self._pending_start_tasks = {}
512
+ self._pending_start_tasks[server_id] = task
513
+
514
+ # Add callback to clean up task reference when done
515
+ def cleanup_task(t):
516
+ if hasattr(self, "_pending_start_tasks"):
517
+ self._pending_start_tasks.pop(server_id, None)
518
+
519
+ task.add_done_callback(cleanup_task)
520
+
521
+ logger.info(f"Scheduled background start for server: {server_id}")
522
+ return True # Return immediately - server will start in background
523
+
524
+ except RuntimeError:
525
+ # No async loop, just enable the server
526
+ managed_server = self._managed_servers.get(server_id)
527
+ if managed_server:
528
+ managed_server.enable()
529
+ self.status_tracker.set_status(server_id, ServerState.RUNNING)
530
+ self.status_tracker.record_start_time(server_id)
531
+ logger.info(f"Enabled server (no async context): {server_id}")
532
+ return True
533
+ return False
534
+
535
+ async def stop_server(self, server_id: str) -> bool:
536
+ """
537
+ Stop a server (disable it and stop the subprocess/connection).
538
+
539
+ This both disables the server AND stops any running process.
540
+ For stdio servers, this stops the subprocess.
541
+ For SSE/HTTP servers, this closes the connection.
542
+
543
+ Args:
544
+ server_id: ID of server to stop
545
+
546
+ Returns:
547
+ True if server was stopped, False if not found
548
+ """
549
+ managed_server = self._managed_servers.get(server_id)
550
+ if managed_server is None:
551
+ logger.warning(f"Attempted to stop non-existent server: {server_id}")
552
+ return False
553
+
554
+ try:
555
+ # First disable the server
556
+ managed_server.disable()
557
+ self.status_tracker.set_status(server_id, ServerState.STOPPED)
558
+ self.status_tracker.record_stop_time(server_id)
559
+
560
+ # Try to actually stop it if we have an async context
561
+ try:
562
+ # Stop the server using the async lifecycle manager
563
+ lifecycle_mgr = get_lifecycle_manager()
564
+ stopped = await lifecycle_mgr.stop_server(server_id)
565
+
566
+ if stopped:
567
+ logger.info(
568
+ f"Stopped server process: {managed_server.config.name} (ID: {server_id})"
569
+ )
570
+ self.status_tracker.record_event(
571
+ server_id,
572
+ "stopped",
573
+ {"message": "Server stopped and process terminated"},
574
+ )
575
+ else:
576
+ logger.info(f"Server {server_id} disabled (no process was running)")
577
+ self.status_tracker.record_event(
578
+ server_id, "disabled", {"message": "Server disabled"}
579
+ )
580
+ except Exception as e:
581
+ # Process stop failed, but server is still disabled
582
+ logger.warning(f"Could not stop process for server {server_id}: {e}")
583
+ self.status_tracker.record_event(
584
+ server_id, "disabled", {"message": "Server disabled"}
585
+ )
586
+
587
+ return True
588
+
589
+ except Exception as e:
590
+ logger.error(f"Failed to stop server {server_id}: {e}")
591
+ self.status_tracker.record_event(
592
+ server_id,
593
+ "stop_error",
594
+ {"error": str(e), "message": f"Error stopping server: {e}"},
595
+ )
596
+ return False
597
+
598
+ def stop_server_sync(self, server_id: str) -> bool:
599
+ """
600
+ Synchronous wrapper for stop_server.
601
+
602
+ IMPORTANT: This schedules the server stop as a background task.
603
+ The server subprocess will stop asynchronously.
604
+ """
605
+ try:
606
+ loop = asyncio.get_running_loop()
607
+ # We're in an async context - schedule the server stop as a background task
608
+ # DO NOT use blocking time.sleep() here as it freezes the event loop!
609
+
610
+ # First, disable the server immediately
611
+ managed_server = self._managed_servers.get(server_id)
612
+ if managed_server:
613
+ managed_server.disable()
614
+ self.status_tracker.set_status(server_id, ServerState.STOPPING)
615
+ self.status_tracker.record_stop_time(server_id)
616
+
617
+ # Schedule the async stop_server to run in the background
618
+ async def stop_server_background():
619
+ try:
620
+ result = await self.stop_server(server_id)
621
+ if result:
622
+ logger.info(f"Background server stop completed: {server_id}")
623
+ return result
624
+ except Exception as e:
625
+ logger.error(f"Background server stop error for {server_id}: {e}")
626
+ return False
627
+
628
+ # Create the task - it will run when the event loop gets control
629
+ task = loop.create_task(
630
+ stop_server_background(), name=f"stop_server_{server_id}"
631
+ )
632
+
633
+ # Store task reference to prevent garbage collection
634
+ if not hasattr(self, "_pending_stop_tasks"):
635
+ self._pending_stop_tasks = {}
636
+ self._pending_stop_tasks[server_id] = task
637
+
638
+ # Add callback to clean up task reference when done
639
+ def cleanup_task(t):
640
+ if hasattr(self, "_pending_stop_tasks"):
641
+ self._pending_stop_tasks.pop(server_id, None)
642
+
643
+ task.add_done_callback(cleanup_task)
644
+
645
+ logger.info(f"Scheduled background stop for server: {server_id}")
646
+ return True # Return immediately - server will stop in background
647
+
648
+ except RuntimeError:
649
+ # No async loop, just disable the server
650
+ managed_server = self._managed_servers.get(server_id)
651
+ if managed_server:
652
+ managed_server.disable()
653
+ self.status_tracker.set_status(server_id, ServerState.STOPPED)
654
+ self.status_tracker.record_stop_time(server_id)
655
+ logger.info(f"Disabled server (no async context): {server_id}")
656
+ return True
657
+ return False
658
+
659
+ def reload_server(self, server_id: str) -> bool:
660
+ """
661
+ Reload a server configuration.
662
+
663
+ Args:
664
+ server_id: ID of server to reload
665
+
666
+ Returns:
667
+ True if server was reloaded, False if not found or failed
668
+ """
669
+ config = self.registry.get(server_id)
670
+ if config is None:
671
+ logger.warning(f"Attempted to reload non-existent server: {server_id}")
672
+ return False
673
+
674
+ try:
675
+ # Remove old managed server
676
+ if server_id in self._managed_servers:
677
+ old_server = self._managed_servers[server_id]
678
+ logger.debug(f"Removing old server instance: {old_server.config.name}")
679
+ del self._managed_servers[server_id]
680
+
681
+ # Create new managed server
682
+ managed_server = ManagedMCPServer(config)
683
+ self._managed_servers[server_id] = managed_server
684
+
685
+ # Update status tracker - always start as STOPPED
686
+ # Servers must be explicitly started with /mcp start
687
+ self.status_tracker.set_status(server_id, ServerState.STOPPED)
688
+
689
+ # Record reload event
690
+ self.status_tracker.record_event(
691
+ server_id, "reloaded", {"message": "Server configuration reloaded"}
692
+ )
693
+
694
+ logger.info(f"Reloaded server: {config.name} (ID: {server_id})")
695
+ return True
696
+
697
+ except Exception as e:
698
+ logger.error(f"Failed to reload server {server_id}: {e}")
699
+ self.status_tracker.set_status(server_id, ServerState.ERROR)
700
+ self.status_tracker.record_event(
701
+ server_id,
702
+ "reload_error",
703
+ {"error": str(e), "message": f"Error reloading server: {e}"},
704
+ )
705
+ return False
706
+
707
+ def remove_server(self, server_id: str) -> bool:
708
+ """
709
+ Remove a server completely.
710
+
711
+ Args:
712
+ server_id: ID of server to remove
713
+
714
+ Returns:
715
+ True if server was removed, False if not found
716
+ """
717
+ # Get server name for logging
718
+ config = self.registry.get(server_id)
719
+ server_name = config.name if config else server_id
720
+
721
+ # Remove from registry
722
+ registry_removed = self.registry.unregister(server_id)
723
+
724
+ # Remove from managed servers
725
+ managed_removed = False
726
+ if server_id in self._managed_servers:
727
+ del self._managed_servers[server_id]
728
+ managed_removed = True
729
+
730
+ # Record removal event if server existed
731
+ if registry_removed or managed_removed:
732
+ self.status_tracker.record_event(
733
+ server_id, "removed", {"message": "Server removed"}
734
+ )
735
+ logger.info(f"Removed server: {server_name} (ID: {server_id})")
736
+ return True
737
+ else:
738
+ logger.warning(f"Attempted to remove non-existent server: {server_id}")
739
+ return False
740
+
741
+ def get_server_status(self, server_id: str) -> Dict[str, Any]:
742
+ """
743
+ Get comprehensive status for a server.
744
+
745
+ Args:
746
+ server_id: ID of server to get status for
747
+
748
+ Returns:
749
+ Dictionary containing comprehensive status information
750
+ """
751
+ # Get basic status from managed server
752
+ managed_server = self._managed_servers.get(server_id)
753
+ if managed_server is None:
754
+ return {
755
+ "server_id": server_id,
756
+ "exists": False,
757
+ "error": "Server not found",
758
+ }
759
+
760
+ try:
761
+ # Get status from managed server
762
+ status = managed_server.get_status()
763
+
764
+ # Add status tracker information
765
+ tracker_summary = self.status_tracker.get_server_summary(server_id)
766
+ recent_events = self.status_tracker.get_events(server_id, limit=5)
767
+
768
+ # Combine all information
769
+ comprehensive_status = {
770
+ **status, # Include all managed server status
771
+ "tracker_state": tracker_summary["state"],
772
+ "tracker_metadata": tracker_summary["metadata"],
773
+ "recent_events_count": tracker_summary["recent_events_count"],
774
+ "tracker_uptime": tracker_summary["uptime"],
775
+ "last_event_time": tracker_summary["last_event_time"],
776
+ "recent_events": [
777
+ {
778
+ "timestamp": event.timestamp.isoformat(),
779
+ "event_type": event.event_type,
780
+ "details": event.details,
781
+ }
782
+ for event in recent_events
783
+ ],
784
+ }
785
+
786
+ return comprehensive_status
787
+
788
+ except Exception as e:
789
+ logger.error(f"Error getting status for server {server_id}: {e}")
790
+ return {"server_id": server_id, "exists": True, "error": str(e)}
791
+
792
+
793
+ # Singleton instance
794
+ _manager_instance: Optional[MCPManager] = None
795
+
796
+
797
+ def get_mcp_manager() -> MCPManager:
798
+ """
799
+ Get the singleton MCPManager instance.
800
+
801
+ Returns:
802
+ The global MCPManager instance
803
+ """
804
+ global _manager_instance
805
+ if _manager_instance is None:
806
+ _manager_instance = MCPManager()
807
+ return _manager_instance