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,308 @@
1
+ """
2
+ MCP Dashboard Implementation
3
+
4
+ Provides visual status dashboard for MCP servers using Rich tables.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from typing import Dict, List, Optional
9
+
10
+ from rich import box
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ from .manager import get_mcp_manager
15
+ from .status_tracker import ServerState
16
+
17
+
18
+ class MCPDashboard:
19
+ """Visual dashboard for MCP server status monitoring.
20
+
21
+ Note: This class uses Rich Console directly for rendering Rich tables.
22
+ This is intentional - Rich tables require Console for proper formatting.
23
+ """
24
+
25
+ def __init__(self):
26
+ """Initialize the MCP Dashboard."""
27
+ # Note: Console is used here specifically for Rich table rendering
28
+ self._console = Console()
29
+
30
+ def render_dashboard(self) -> Table:
31
+ """
32
+ Render the main MCP server status dashboard
33
+
34
+ Returns:
35
+ Table: Rich table with server status information
36
+ """
37
+ # Create the main table
38
+ table = Table(
39
+ title="MCP Server Status Dashboard",
40
+ box=box.ROUNDED,
41
+ show_header=True,
42
+ header_style="bold blue",
43
+ title_style="bold cyan",
44
+ )
45
+
46
+ # Define columns
47
+ table.add_column("Name", style="white", no_wrap=True, min_width=10)
48
+ table.add_column("Type", style="white", no_wrap=True, width=8)
49
+ table.add_column("State", style="white", no_wrap=True, width=8)
50
+ table.add_column("Health", style="white", no_wrap=True, width=8)
51
+ table.add_column("Uptime", style="white", no_wrap=True, width=10)
52
+ table.add_column("Latency", style="white", no_wrap=True, width=10)
53
+
54
+ # Get manager and server info
55
+ try:
56
+ manager = get_mcp_manager()
57
+ servers = manager.list_servers()
58
+
59
+ if not servers:
60
+ # Empty state
61
+ table.add_row(
62
+ "[dim]No servers configured[/dim]", "-", "-", "-", "-", "-"
63
+ )
64
+ else:
65
+ # Add row for each server
66
+ for server in servers:
67
+ row_data = self.render_server_row(server)
68
+ table.add_row(*row_data)
69
+
70
+ except Exception as e:
71
+ # Error state
72
+ table.add_row(
73
+ "[red]Error loading servers[/red]",
74
+ "-",
75
+ "-",
76
+ "-",
77
+ "-",
78
+ f"[red]{str(e)}[/red]",
79
+ )
80
+
81
+ return table
82
+
83
+ def render_server_row(self, server) -> List[str]:
84
+ """
85
+ Render a single server row for the dashboard
86
+
87
+ Args:
88
+ server: ServerInfo object with server details
89
+
90
+ Returns:
91
+ List[str]: Formatted row data for the table
92
+ """
93
+ # Server name
94
+ name = server.name or server.id[:8]
95
+
96
+ # Server type
97
+ server_type = server.type.upper() if server.type else "UNK"
98
+
99
+ # State indicator
100
+ state_indicator = self.render_state_indicator(server.state)
101
+
102
+ # Health indicator
103
+ health_indicator = self.render_health_indicator(server.health)
104
+
105
+ # Uptime
106
+ uptime_str = self.format_uptime(server.start_time) if server.start_time else "-"
107
+
108
+ # Latency
109
+ latency_str = (
110
+ self.format_latency(server.latency_ms)
111
+ if server.latency_ms is not None
112
+ else "-"
113
+ )
114
+
115
+ return [
116
+ name,
117
+ server_type,
118
+ state_indicator,
119
+ health_indicator,
120
+ uptime_str,
121
+ latency_str,
122
+ ]
123
+
124
+ def render_health_indicator(self, health: Optional[Dict]) -> str:
125
+ """
126
+ Render health status indicator
127
+
128
+ Args:
129
+ health: Health status dictionary or None
130
+
131
+ Returns:
132
+ str: Formatted health indicator with color
133
+ """
134
+ if not health:
135
+ return "[dim]?[/dim]"
136
+
137
+ is_healthy = health.get("is_healthy", False)
138
+ error = health.get("error")
139
+
140
+ if is_healthy:
141
+ return "[green]✓[/green]"
142
+ elif error:
143
+ return "[red]✗[/red]"
144
+ else:
145
+ return "[yellow]?[/yellow]"
146
+
147
+ def render_state_indicator(self, state: ServerState) -> str:
148
+ """
149
+ Render server state indicator
150
+
151
+ Args:
152
+ state: Current server state
153
+
154
+ Returns:
155
+ str: Formatted state indicator with color and symbol
156
+ """
157
+ indicators = {
158
+ ServerState.RUNNING: "[green]✓ Run[/green]",
159
+ ServerState.STOPPED: "[red]✗ Stop[/red]",
160
+ ServerState.ERROR: "[red]⚠ Err[/red]",
161
+ ServerState.STARTING: "[yellow]⏳ Start[/yellow]",
162
+ ServerState.STOPPING: "[yellow]⏳ Stop[/yellow]",
163
+ ServerState.QUARANTINED: "[yellow]⏸ Quar[/yellow]",
164
+ }
165
+
166
+ return indicators.get(state, "[dim]? Unk[/dim]")
167
+
168
+ def render_metrics_summary(self, metrics: Dict) -> str:
169
+ """
170
+ Render a summary of server metrics
171
+
172
+ Args:
173
+ metrics: Dictionary of server metrics
174
+
175
+ Returns:
176
+ str: Formatted metrics summary
177
+ """
178
+ if not metrics:
179
+ return "No metrics"
180
+
181
+ parts = []
182
+
183
+ # Request count
184
+ if "request_count" in metrics:
185
+ parts.append(f"Req: {metrics['request_count']}")
186
+
187
+ # Error rate
188
+ if "error_rate" in metrics:
189
+ error_rate = metrics["error_rate"]
190
+ if error_rate > 0.1: # 10%
191
+ parts.append(f"[red]Err: {error_rate:.1%}[/red]")
192
+ elif error_rate > 0.05: # 5%
193
+ parts.append(f"[yellow]Err: {error_rate:.1%}[/yellow]")
194
+ else:
195
+ parts.append(f"[green]Err: {error_rate:.1%}[/green]")
196
+
197
+ # Response time
198
+ if "avg_response_time" in metrics:
199
+ avg_time = metrics["avg_response_time"]
200
+ parts.append(f"Avg: {avg_time:.0f}ms")
201
+
202
+ return " | ".join(parts) if parts else "No data"
203
+
204
+ def format_uptime(self, start_time: datetime) -> str:
205
+ """
206
+ Format uptime duration in human readable format
207
+
208
+ Args:
209
+ start_time: Server start timestamp
210
+
211
+ Returns:
212
+ str: Formatted uptime string (e.g., "2h 15m")
213
+ """
214
+ if not start_time:
215
+ return "-"
216
+
217
+ try:
218
+ uptime = datetime.now() - start_time
219
+
220
+ # Handle negative uptime (clock skew, etc.)
221
+ if uptime.total_seconds() < 0:
222
+ return "0s"
223
+
224
+ # Format based on duration
225
+ total_seconds = int(uptime.total_seconds())
226
+
227
+ if total_seconds < 60: # Less than 1 minute
228
+ return f"{total_seconds}s"
229
+ elif total_seconds < 3600: # Less than 1 hour
230
+ minutes = total_seconds // 60
231
+ seconds = total_seconds % 60
232
+ if seconds > 0:
233
+ return f"{minutes}m {seconds}s"
234
+ else:
235
+ return f"{minutes}m"
236
+ elif total_seconds < 86400: # Less than 1 day
237
+ hours = total_seconds // 3600
238
+ minutes = (total_seconds % 3600) // 60
239
+ if minutes > 0:
240
+ return f"{hours}h {minutes}m"
241
+ else:
242
+ return f"{hours}h"
243
+ else: # 1 day or more
244
+ days = total_seconds // 86400
245
+ hours = (total_seconds % 86400) // 3600
246
+ if hours > 0:
247
+ return f"{days}d {hours}h"
248
+ else:
249
+ return f"{days}d"
250
+
251
+ except Exception:
252
+ return "?"
253
+
254
+ def format_latency(self, latency_ms: float) -> str:
255
+ """
256
+ Format latency in human readable format
257
+
258
+ Args:
259
+ latency_ms: Latency in milliseconds
260
+
261
+ Returns:
262
+ str: Formatted latency string with color coding
263
+ """
264
+ if latency_ms is None:
265
+ return "-"
266
+
267
+ try:
268
+ if latency_ms < 0:
269
+ return "invalid"
270
+ elif latency_ms < 50: # Fast
271
+ return f"[green]{latency_ms:.0f}ms[/green]"
272
+ elif latency_ms < 200: # Acceptable
273
+ return f"[yellow]{latency_ms:.0f}ms[/yellow]"
274
+ elif latency_ms < 1000: # Slow
275
+ return f"[red]{latency_ms:.0f}ms[/red]"
276
+ elif latency_ms >= 30000: # Timeout (30s+)
277
+ return "[red]timeout[/red]"
278
+ else: # Very slow
279
+ seconds = latency_ms / 1000
280
+ return f"[red]{seconds:.1f}s[/red]"
281
+
282
+ except (ValueError, TypeError):
283
+ return "error"
284
+
285
+ def print_dashboard(self) -> None:
286
+ """Print the dashboard to console.
287
+
288
+ Note: Uses Rich Console directly for table rendering - Rich tables
289
+ require Console for proper formatting with colors and borders.
290
+ """
291
+ table = self.render_dashboard()
292
+ self._console.print(table)
293
+ self._console.print() # Add spacing
294
+
295
+ def get_dashboard_string(self) -> str:
296
+ """
297
+ Get dashboard as a string for programmatic use
298
+
299
+ Returns:
300
+ str: Dashboard rendered as plain text
301
+ """
302
+ # Create a console that captures output
303
+ console = Console(file=None, width=80)
304
+
305
+ with console.capture() as capture:
306
+ console.print(self.render_dashboard())
307
+
308
+ return capture.get()
@@ -0,0 +1,407 @@
1
+ """
2
+ MCP Error Isolation System
3
+
4
+ This module provides error isolation for MCP server calls to prevent
5
+ server errors from crashing the application. It implements quarantine
6
+ logic with exponential backoff for failed servers.
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timedelta
13
+ from enum import Enum
14
+ from typing import Any, Callable, Dict, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class ErrorStats:
21
+ """Statistics for MCP server errors and quarantine status."""
22
+
23
+ total_errors: int = 0
24
+ consecutive_errors: int = 0
25
+ last_error: Optional[datetime] = None
26
+ error_types: Dict[str, int] = field(default_factory=dict)
27
+ quarantine_count: int = 0
28
+ quarantine_until: Optional[datetime] = None
29
+
30
+
31
+ class ErrorCategory(Enum):
32
+ """Categories of errors that can be isolated."""
33
+
34
+ NETWORK = "network"
35
+ PROTOCOL = "protocol"
36
+ SERVER = "server"
37
+ RATE_LIMIT = "rate_limit"
38
+ AUTHENTICATION = "authentication"
39
+ UNKNOWN = "unknown"
40
+
41
+
42
+ class MCPErrorIsolator:
43
+ """
44
+ Isolates MCP server errors to prevent application crashes.
45
+
46
+ Features:
47
+ - Quarantine servers after consecutive failures
48
+ - Exponential backoff for quarantine duration
49
+ - Error categorization and tracking
50
+ - Automatic recovery after successful calls
51
+ """
52
+
53
+ def __init__(self, quarantine_threshold: int = 5, max_quarantine_minutes: int = 30):
54
+ """
55
+ Initialize the error isolator.
56
+
57
+ Args:
58
+ quarantine_threshold: Number of consecutive errors to trigger quarantine
59
+ max_quarantine_minutes: Maximum quarantine duration in minutes
60
+ """
61
+ self.quarantine_threshold = quarantine_threshold
62
+ self.max_quarantine_duration = timedelta(minutes=max_quarantine_minutes)
63
+ self.server_stats: Dict[str, ErrorStats] = {}
64
+ self._lock = asyncio.Lock()
65
+
66
+ logger.info(
67
+ f"MCPErrorIsolator initialized with threshold={quarantine_threshold}, "
68
+ f"max_quarantine={max_quarantine_minutes}min"
69
+ )
70
+
71
+ async def isolated_call(
72
+ self, server_id: str, func: Callable, *args, **kwargs
73
+ ) -> Any:
74
+ """
75
+ Execute a function call with error isolation.
76
+
77
+ Args:
78
+ server_id: ID of the MCP server making the call
79
+ func: Function to execute
80
+ *args: Arguments for the function
81
+ **kwargs: Keyword arguments for the function
82
+
83
+ Returns:
84
+ Result of the function call
85
+
86
+ Raises:
87
+ Exception: If the server is quarantined or the call fails
88
+ """
89
+ async with self._lock:
90
+ # Check if server is quarantined
91
+ if self.is_quarantined(server_id):
92
+ quarantine_until = self.server_stats[server_id].quarantine_until
93
+ raise QuarantinedServerError(
94
+ f"Server {server_id} is quarantined until {quarantine_until}"
95
+ )
96
+
97
+ try:
98
+ # Execute the function
99
+ if asyncio.iscoroutinefunction(func):
100
+ result = await func(*args, **kwargs)
101
+ else:
102
+ result = func(*args, **kwargs)
103
+
104
+ # Record success
105
+ async with self._lock:
106
+ await self._record_success(server_id)
107
+
108
+ return result
109
+
110
+ except Exception as error:
111
+ # Record and categorize the error
112
+ async with self._lock:
113
+ await self._record_error(server_id, error)
114
+
115
+ # Re-raise the error
116
+ raise
117
+
118
+ async def quarantine_server(self, server_id: str, duration: int) -> None:
119
+ """
120
+ Manually quarantine a server for a specific duration.
121
+
122
+ Args:
123
+ server_id: ID of the server to quarantine
124
+ duration: Quarantine duration in seconds
125
+ """
126
+ async with self._lock:
127
+ stats = self._get_or_create_stats(server_id)
128
+ stats.quarantine_until = datetime.now() + timedelta(seconds=duration)
129
+ stats.quarantine_count += 1
130
+
131
+ logger.warning(
132
+ f"Server {server_id} quarantined for {duration}s "
133
+ f"(count: {stats.quarantine_count})"
134
+ )
135
+
136
+ def is_quarantined(self, server_id: str) -> bool:
137
+ """
138
+ Check if a server is currently quarantined.
139
+
140
+ Args:
141
+ server_id: ID of the server to check
142
+
143
+ Returns:
144
+ True if the server is quarantined, False otherwise
145
+ """
146
+ if server_id not in self.server_stats:
147
+ return False
148
+
149
+ stats = self.server_stats[server_id]
150
+ if stats.quarantine_until is None:
151
+ return False
152
+
153
+ # Check if quarantine has expired
154
+ if datetime.now() >= stats.quarantine_until:
155
+ stats.quarantine_until = None
156
+ return False
157
+
158
+ return True
159
+
160
+ async def release_quarantine(self, server_id: str) -> None:
161
+ """
162
+ Manually release a server from quarantine.
163
+
164
+ Args:
165
+ server_id: ID of the server to release
166
+ """
167
+ async with self._lock:
168
+ if server_id in self.server_stats:
169
+ self.server_stats[server_id].quarantine_until = None
170
+ logger.info(f"Server {server_id} released from quarantine")
171
+
172
+ def get_error_stats(self, server_id: str) -> ErrorStats:
173
+ """
174
+ Get error statistics for a server.
175
+
176
+ Args:
177
+ server_id: ID of the server
178
+
179
+ Returns:
180
+ ErrorStats object with current statistics
181
+ """
182
+ if server_id not in self.server_stats:
183
+ return ErrorStats()
184
+
185
+ return self.server_stats[server_id]
186
+
187
+ def should_quarantine(self, server_id: str) -> bool:
188
+ """
189
+ Check if a server should be quarantined based on error count.
190
+
191
+ Args:
192
+ server_id: ID of the server to check
193
+
194
+ Returns:
195
+ True if the server should be quarantined
196
+ """
197
+ if server_id not in self.server_stats:
198
+ return False
199
+
200
+ stats = self.server_stats[server_id]
201
+ return stats.consecutive_errors >= self.quarantine_threshold
202
+
203
+ def _get_or_create_stats(self, server_id: str) -> ErrorStats:
204
+ """Get or create error stats for a server."""
205
+ if server_id not in self.server_stats:
206
+ self.server_stats[server_id] = ErrorStats()
207
+ return self.server_stats[server_id]
208
+
209
+ async def _record_success(self, server_id: str) -> None:
210
+ """Record a successful call and reset consecutive error count."""
211
+ stats = self._get_or_create_stats(server_id)
212
+ stats.consecutive_errors = 0
213
+
214
+ logger.debug(
215
+ f"Success recorded for server {server_id}, consecutive errors reset"
216
+ )
217
+
218
+ async def _record_error(self, server_id: str, error: Exception) -> None:
219
+ """Record an error and potentially quarantine the server."""
220
+ stats = self._get_or_create_stats(server_id)
221
+
222
+ # Update error statistics
223
+ stats.total_errors += 1
224
+ stats.consecutive_errors += 1
225
+ stats.last_error = datetime.now()
226
+
227
+ # Categorize the error
228
+ error_category = self._categorize_error(error)
229
+ error_type = error_category.value
230
+ stats.error_types[error_type] = stats.error_types.get(error_type, 0) + 1
231
+
232
+ logger.warning(
233
+ f"Error recorded for server {server_id}: {error_type} - {str(error)} "
234
+ f"(consecutive: {stats.consecutive_errors})"
235
+ )
236
+
237
+ # Check if quarantine is needed
238
+ if self.should_quarantine(server_id):
239
+ quarantine_duration = self._calculate_quarantine_duration(
240
+ stats.quarantine_count
241
+ )
242
+ stats.quarantine_until = datetime.now() + timedelta(
243
+ seconds=quarantine_duration
244
+ )
245
+ stats.quarantine_count += 1
246
+
247
+ logger.error(
248
+ f"Server {server_id} quarantined for {quarantine_duration}s "
249
+ f"after {stats.consecutive_errors} consecutive errors "
250
+ f"(quarantine count: {stats.quarantine_count})"
251
+ )
252
+
253
+ def _categorize_error(self, error: Exception) -> ErrorCategory:
254
+ """
255
+ Categorize an error based on its type and properties.
256
+
257
+ Args:
258
+ error: The exception to categorize
259
+
260
+ Returns:
261
+ ErrorCategory enum value
262
+ """
263
+ error_type = type(error).__name__.lower()
264
+ error_message = str(error).lower()
265
+
266
+ # Network errors
267
+ if any(
268
+ keyword in error_type
269
+ for keyword in ["connection", "timeout", "network", "socket", "dns", "ssl"]
270
+ ):
271
+ return ErrorCategory.NETWORK
272
+
273
+ if any(
274
+ keyword in error_message
275
+ for keyword in [
276
+ "connection",
277
+ "timeout",
278
+ "network",
279
+ "unreachable",
280
+ "refused",
281
+ ]
282
+ ):
283
+ return ErrorCategory.NETWORK
284
+
285
+ # Protocol errors
286
+ if any(
287
+ keyword in error_type
288
+ for keyword in [
289
+ "json",
290
+ "decode",
291
+ "parse",
292
+ "schema",
293
+ "validation",
294
+ "protocol",
295
+ ]
296
+ ):
297
+ return ErrorCategory.PROTOCOL
298
+
299
+ if any(
300
+ keyword in error_message
301
+ for keyword in ["json", "decode", "parse", "invalid", "malformed", "schema"]
302
+ ):
303
+ return ErrorCategory.PROTOCOL
304
+
305
+ # Authentication errors
306
+ if any(
307
+ keyword in error_type
308
+ for keyword in ["auth", "permission", "unauthorized", "forbidden"]
309
+ ):
310
+ return ErrorCategory.AUTHENTICATION
311
+
312
+ if any(
313
+ keyword in error_message
314
+ for keyword in [
315
+ "401",
316
+ "403",
317
+ "unauthorized",
318
+ "forbidden",
319
+ "authentication",
320
+ "permission",
321
+ ]
322
+ ):
323
+ return ErrorCategory.AUTHENTICATION
324
+
325
+ # Rate limit errors
326
+ if any(keyword in error_type for keyword in ["rate", "limit", "throttle"]):
327
+ return ErrorCategory.RATE_LIMIT
328
+
329
+ if any(
330
+ keyword in error_message
331
+ for keyword in ["429", "rate limit", "too many requests", "throttle"]
332
+ ):
333
+ return ErrorCategory.RATE_LIMIT
334
+
335
+ # Server errors (5xx responses)
336
+ if any(
337
+ keyword in error_message
338
+ for keyword in [
339
+ "500",
340
+ "501",
341
+ "502",
342
+ "503",
343
+ "504",
344
+ "505",
345
+ "internal server error",
346
+ "bad gateway",
347
+ "service unavailable",
348
+ "gateway timeout",
349
+ ]
350
+ ):
351
+ return ErrorCategory.SERVER
352
+
353
+ if any(keyword in error_type for keyword in ["server", "internal"]):
354
+ return ErrorCategory.SERVER
355
+
356
+ # Default to unknown
357
+ return ErrorCategory.UNKNOWN
358
+
359
+ def _calculate_quarantine_duration(self, quarantine_count: int) -> int:
360
+ """
361
+ Calculate quarantine duration using exponential backoff.
362
+
363
+ Args:
364
+ quarantine_count: Number of times this server has been quarantined
365
+
366
+ Returns:
367
+ Quarantine duration in seconds
368
+ """
369
+ # Base duration: 30 seconds
370
+ base_duration = 30
371
+
372
+ # Exponential backoff: 30s, 60s, 120s, 240s, etc.
373
+ duration = base_duration * (2**quarantine_count)
374
+
375
+ # Cap at maximum duration (convert to seconds)
376
+ max_seconds = int(self.max_quarantine_duration.total_seconds())
377
+ duration = min(duration, max_seconds)
378
+
379
+ logger.debug(
380
+ f"Calculated quarantine duration: {duration}s "
381
+ f"(count: {quarantine_count}, max: {max_seconds}s)"
382
+ )
383
+
384
+ return duration
385
+
386
+
387
+ class QuarantinedServerError(Exception):
388
+ """Raised when attempting to call a quarantined server."""
389
+
390
+ pass
391
+
392
+
393
+ # Global isolator instance
394
+ _isolator_instance: Optional[MCPErrorIsolator] = None
395
+
396
+
397
+ def get_error_isolator() -> MCPErrorIsolator:
398
+ """
399
+ Get the global MCPErrorIsolator instance.
400
+
401
+ Returns:
402
+ MCPErrorIsolator instance
403
+ """
404
+ global _isolator_instance
405
+ if _isolator_instance is None:
406
+ _isolator_instance = MCPErrorIsolator()
407
+ return _isolator_instance