claude-team-mcp 0.4.0__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 (42) hide show
  1. claude_team_mcp/__init__.py +24 -0
  2. claude_team_mcp/__main__.py +8 -0
  3. claude_team_mcp/cli_backends/__init__.py +44 -0
  4. claude_team_mcp/cli_backends/base.py +132 -0
  5. claude_team_mcp/cli_backends/claude.py +110 -0
  6. claude_team_mcp/cli_backends/codex.py +110 -0
  7. claude_team_mcp/colors.py +108 -0
  8. claude_team_mcp/formatting.py +120 -0
  9. claude_team_mcp/idle_detection.py +488 -0
  10. claude_team_mcp/iterm_utils.py +1119 -0
  11. claude_team_mcp/names.py +427 -0
  12. claude_team_mcp/profile.py +364 -0
  13. claude_team_mcp/registry.py +426 -0
  14. claude_team_mcp/schemas/__init__.py +5 -0
  15. claude_team_mcp/schemas/codex.py +267 -0
  16. claude_team_mcp/server.py +390 -0
  17. claude_team_mcp/session_state.py +1058 -0
  18. claude_team_mcp/subprocess_cache.py +119 -0
  19. claude_team_mcp/tools/__init__.py +52 -0
  20. claude_team_mcp/tools/adopt_worker.py +122 -0
  21. claude_team_mcp/tools/annotate_worker.py +57 -0
  22. claude_team_mcp/tools/bd_help.py +42 -0
  23. claude_team_mcp/tools/check_idle_workers.py +98 -0
  24. claude_team_mcp/tools/close_workers.py +194 -0
  25. claude_team_mcp/tools/discover_workers.py +129 -0
  26. claude_team_mcp/tools/examine_worker.py +56 -0
  27. claude_team_mcp/tools/list_workers.py +76 -0
  28. claude_team_mcp/tools/list_worktrees.py +106 -0
  29. claude_team_mcp/tools/message_workers.py +311 -0
  30. claude_team_mcp/tools/read_worker_logs.py +158 -0
  31. claude_team_mcp/tools/spawn_workers.py +634 -0
  32. claude_team_mcp/tools/wait_idle_workers.py +148 -0
  33. claude_team_mcp/utils/__init__.py +17 -0
  34. claude_team_mcp/utils/constants.py +87 -0
  35. claude_team_mcp/utils/errors.py +87 -0
  36. claude_team_mcp/utils/worktree_detection.py +79 -0
  37. claude_team_mcp/worker_prompt.py +350 -0
  38. claude_team_mcp/worktree.py +532 -0
  39. claude_team_mcp-0.4.0.dist-info/METADATA +414 -0
  40. claude_team_mcp-0.4.0.dist-info/RECORD +42 -0
  41. claude_team_mcp-0.4.0.dist-info/WHEEL +4 -0
  42. claude_team_mcp-0.4.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,390 @@
1
+ """
2
+ Claude Team MCP Server
3
+
4
+ FastMCP-based server for managing multiple Claude Code sessions via iTerm2.
5
+ Allows a "manager" Claude Code session to spawn and coordinate multiple
6
+ "worker" Claude Code sessions.
7
+ """
8
+
9
+ import logging
10
+ from collections.abc import AsyncIterator
11
+ from contextlib import asynccontextmanager
12
+ from dataclasses import dataclass
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from iterm2.app import App as ItermApp
17
+ from iterm2.connection import Connection as ItermConnection
18
+
19
+ from mcp.server.fastmcp import Context, FastMCP
20
+ from mcp.server.session import ServerSession
21
+
22
+ from .iterm_utils import read_screen_text
23
+ from .registry import SessionRegistry
24
+ from .tools import register_all_tools
25
+ from .utils import error_response, HINTS
26
+
27
+ # Configure logging
28
+ logging.basicConfig(
29
+ level=logging.DEBUG,
30
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
31
+ )
32
+ logger = logging.getLogger("claude-team-mcp")
33
+ # Add file handler for debugging
34
+ _fh = logging.FileHandler("/tmp/claude-team-debug.log")
35
+ _fh.setLevel(logging.DEBUG)
36
+ _fh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
37
+ logger.addHandler(_fh)
38
+ logging.getLogger().addHandler(_fh) # Also capture root logger
39
+ logger.info("=== Claude Team MCP Server Starting ===")
40
+
41
+
42
+ # =============================================================================
43
+ # Singleton Registry (persists across MCP sessions for HTTP mode)
44
+ # =============================================================================
45
+
46
+ _global_registry: SessionRegistry | None = None
47
+
48
+
49
+ def get_global_registry() -> SessionRegistry:
50
+ """Get or create the global singleton registry."""
51
+ global _global_registry
52
+ if _global_registry is None:
53
+ _global_registry = SessionRegistry()
54
+ logger.info("Created global singleton registry")
55
+ return _global_registry
56
+
57
+
58
+ # =============================================================================
59
+ # Application Context
60
+ # =============================================================================
61
+
62
+
63
+ @dataclass
64
+ class AppContext:
65
+ """
66
+ Application context shared across all tool invocations.
67
+
68
+ Maintains the iTerm2 connection and registry of managed sessions.
69
+ This is the persistent state that makes the MCP server useful.
70
+ """
71
+
72
+ iterm_connection: "ItermConnection"
73
+ iterm_app: "ItermApp"
74
+ registry: SessionRegistry
75
+
76
+
77
+ # =============================================================================
78
+ # Lifespan Management
79
+ # =============================================================================
80
+
81
+
82
+ async def refresh_iterm_connection() -> tuple["ItermConnection", "ItermApp"]:
83
+ """
84
+ Create a fresh iTerm2 connection.
85
+
86
+ The iTerm2 Python API uses websockets with ping_interval=None, meaning
87
+ connections can go stale without any keepalive mechanism. This function
88
+ creates a new connection when needed.
89
+
90
+ Returns:
91
+ Tuple of (connection, app)
92
+
93
+ Raises:
94
+ RuntimeError: If connection fails
95
+ """
96
+ from iterm2.app import async_get_app
97
+ from iterm2.connection import Connection
98
+
99
+ logger.debug("Creating fresh iTerm2 connection...")
100
+ try:
101
+ connection = await Connection.async_create()
102
+ app = await async_get_app(connection)
103
+ if app is None:
104
+ raise RuntimeError("Could not get iTerm2 app")
105
+ logger.debug("Fresh iTerm2 connection established")
106
+ return connection, app
107
+ except Exception as e:
108
+ logger.error(f"Failed to refresh iTerm2 connection: {e}")
109
+ raise RuntimeError("Could not connect to iTerm2") from e
110
+
111
+
112
+ async def ensure_connection(app_ctx: "AppContext") -> tuple["ItermConnection", "ItermApp"]:
113
+ """
114
+ Ensure we have a working iTerm2 connection, refreshing if stale.
115
+
116
+ The iTerm2 websocket connection can go stale due to lack of keepalive
117
+ (ping_interval=None in the iterm2 library). This function tests the
118
+ connection and refreshes it if needed.
119
+
120
+ Args:
121
+ app_ctx: The application context containing connection and app
122
+
123
+ Returns:
124
+ Tuple of (connection, app) - either existing or refreshed
125
+ """
126
+ from iterm2.app import async_get_app
127
+
128
+ connection = app_ctx.iterm_connection
129
+ app = app_ctx.iterm_app
130
+
131
+ # Test if connection is still alive by trying a simple operation
132
+ try:
133
+ # async_get_app is a lightweight call that tests the connection
134
+ refreshed_app = await async_get_app(connection)
135
+ if refreshed_app is not None:
136
+ return connection, refreshed_app
137
+ # App is None, need to refresh
138
+ raise RuntimeError("App is None, refreshing connection")
139
+ except Exception as e:
140
+ logger.warning(f"iTerm2 connection appears stale ({e}), refreshing...")
141
+ # Connection is dead, create a new one
142
+ connection, app = await refresh_iterm_connection()
143
+ # Update the context with fresh connection
144
+ app_ctx.iterm_connection = connection
145
+ app_ctx.iterm_app = app
146
+ return connection, app
147
+
148
+
149
+ @asynccontextmanager
150
+ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
151
+ """
152
+ Manage iTerm2 connection lifecycle.
153
+
154
+ Connects to iTerm2 on startup and maintains the connection
155
+ for the duration of the server's lifetime.
156
+
157
+ Note: The iTerm2 Python API uses websockets with ping_interval=None,
158
+ meaning connections can go stale. Individual tool functions should use
159
+ ensure_connection() before making iTerm2 API calls that use the
160
+ connection directly.
161
+ """
162
+ logger.info("Claude Team MCP Server starting...")
163
+
164
+ # Import iterm2 here to fail fast if not available
165
+ try:
166
+ from iterm2.app import async_get_app
167
+ from iterm2.connection import Connection
168
+ except ImportError as e:
169
+ logger.error(
170
+ "iterm2 package not found. Install with: uv add iterm2\n"
171
+ "Also enable: iTerm2 → Preferences → General → Magic → Enable Python API"
172
+ )
173
+ raise RuntimeError("iterm2 package required") from e
174
+
175
+ # Connect to iTerm2
176
+ logger.info("Connecting to iTerm2...")
177
+ try:
178
+ connection = await Connection.async_create()
179
+ app = await async_get_app(connection)
180
+ if app is None:
181
+ raise RuntimeError("Could not get iTerm2 app")
182
+ logger.info("Connected to iTerm2 successfully")
183
+ except Exception as e:
184
+ logger.error(f"Failed to connect to iTerm2: {e}")
185
+ logger.error("Make sure iTerm2 is running and Python API is enabled")
186
+ raise RuntimeError("Could not connect to iTerm2") from e
187
+
188
+ # Create application context with singleton registry (persists across sessions)
189
+ ctx = AppContext(
190
+ iterm_connection=connection,
191
+ iterm_app=app,
192
+ registry=get_global_registry(),
193
+ )
194
+
195
+ try:
196
+ yield ctx
197
+ finally:
198
+ # Cleanup: close any remaining sessions gracefully
199
+ logger.info("Claude Team MCP Server shutting down...")
200
+ if ctx.registry.count() > 0:
201
+ logger.info(f"Cleaning up {ctx.registry.count()} managed session(s)...")
202
+ logger.info("Shutdown complete")
203
+
204
+
205
+ # =============================================================================
206
+ # FastMCP Server Factory
207
+ # =============================================================================
208
+
209
+
210
+ def create_mcp_server(host: str = "127.0.0.1", port: int = 8766) -> FastMCP:
211
+ """Create and configure the FastMCP server instance."""
212
+ server = FastMCP(
213
+ "Claude Team Manager",
214
+ lifespan=app_lifespan,
215
+ host=host,
216
+ port=port,
217
+ )
218
+ # Register all tools from the tools package
219
+ register_all_tools(server, ensure_connection)
220
+ return server
221
+
222
+
223
+ # Default server instance for stdio mode (backwards compatibility)
224
+ mcp = create_mcp_server()
225
+
226
+
227
+ # =============================================================================
228
+ # MCP Resources
229
+ # =============================================================================
230
+
231
+
232
+ @mcp.resource("sessions://list")
233
+ async def resource_sessions(ctx: Context[ServerSession, AppContext]) -> list[dict]:
234
+ """
235
+ List all managed Claude Code sessions.
236
+
237
+ Returns a list of session summaries including ID, name, project path,
238
+ status, and conversation stats if available. This is a read-only
239
+ resource alternative to the list_workers tool.
240
+ """
241
+ app_ctx = ctx.request_context.lifespan_context
242
+ registry = app_ctx.registry
243
+
244
+ sessions = registry.list_all()
245
+ results = []
246
+
247
+ for session in sessions:
248
+ info = session.to_dict()
249
+ # Add conversation stats if JSONL is available
250
+ state = session.get_conversation_state()
251
+ if state:
252
+ info["message_count"] = state.message_count
253
+ # Check idle using stop hook detection
254
+ info["is_idle"] = session.is_idle()
255
+ results.append(info)
256
+
257
+ return results
258
+
259
+
260
+ @mcp.resource("sessions://{session_id}/status")
261
+ async def resource_session_status(
262
+ session_id: str, ctx: Context[ServerSession, AppContext]
263
+ ) -> dict:
264
+ """
265
+ Get detailed status of a specific Claude Code session.
266
+
267
+ Returns comprehensive information including session metadata,
268
+ conversation statistics, and processing state. Use the /screen
269
+ resource to get terminal screen content.
270
+
271
+ Args:
272
+ session_id: ID of the target session
273
+ """
274
+ app_ctx = ctx.request_context.lifespan_context
275
+ registry = app_ctx.registry
276
+
277
+ session = registry.get(session_id)
278
+ if not session:
279
+ return error_response(
280
+ f"Session not found: {session_id}",
281
+ hint=HINTS["session_not_found"],
282
+ )
283
+
284
+ result = session.to_dict()
285
+
286
+ # Get conversation stats from JSONL
287
+ stats = session.get_conversation_stats()
288
+ result["conversation_stats"] = stats
289
+ result["message_count"] = stats["total_messages"] if stats else 0
290
+
291
+ # Check idle using stop hook detection
292
+ result["is_idle"] = session.is_idle()
293
+
294
+ return result
295
+
296
+
297
+ @mcp.resource("sessions://{session_id}/screen")
298
+ async def resource_session_screen(
299
+ session_id: str, ctx: Context[ServerSession, AppContext]
300
+ ) -> dict:
301
+ """
302
+ Get the current terminal screen content for a session.
303
+
304
+ Returns the visible text in the iTerm2 pane for the specified session.
305
+ Useful for checking what Claude is currently displaying or doing.
306
+
307
+ Args:
308
+ session_id: ID of the target session
309
+ """
310
+ app_ctx = ctx.request_context.lifespan_context
311
+ registry = app_ctx.registry
312
+
313
+ session = registry.get(session_id)
314
+ if not session:
315
+ return error_response(
316
+ f"Session not found: {session_id}",
317
+ hint=HINTS["session_not_found"],
318
+ )
319
+
320
+ try:
321
+ screen_text = await read_screen_text(session.iterm_session)
322
+ # Get non-empty lines
323
+ lines = [line for line in screen_text.split("\n") if line.strip()]
324
+
325
+ return {
326
+ "session_id": session_id,
327
+ "screen_content": screen_text,
328
+ "screen_preview": "\n".join(lines[-15:]) if lines else "",
329
+ "line_count": len(lines),
330
+ "is_responsive": True,
331
+ }
332
+ except Exception as e:
333
+ return error_response(
334
+ f"Could not read screen: {e}",
335
+ hint=HINTS["iterm_connection"],
336
+ session_id=session_id,
337
+ is_responsive=False,
338
+ )
339
+
340
+
341
+ # =============================================================================
342
+ # Server Entry Point
343
+ # =============================================================================
344
+
345
+
346
+ def run_server(transport: str = "stdio", port: int = 8766):
347
+ """
348
+ Run the MCP server.
349
+
350
+ Args:
351
+ transport: Transport mode - "stdio" or "streamable-http"
352
+ port: Port for HTTP transport (default 8766)
353
+ """
354
+ if transport == "streamable-http":
355
+ logger.info(f"Starting Claude Team MCP Server (HTTP on port {port})...")
356
+ # Create server with configured port for HTTP mode
357
+ server = create_mcp_server(host="127.0.0.1", port=port)
358
+ server.run(transport="streamable-http")
359
+ else:
360
+ logger.info("Starting Claude Team MCP Server (stdio)...")
361
+ mcp.run(transport="stdio")
362
+
363
+
364
+ def main():
365
+ """CLI entry point with argument parsing."""
366
+ import argparse
367
+
368
+ parser = argparse.ArgumentParser(description="Claude Team MCP Server")
369
+ parser.add_argument(
370
+ "--http",
371
+ action="store_true",
372
+ help="Run in HTTP mode (streamable-http) instead of stdio",
373
+ )
374
+ parser.add_argument(
375
+ "--port",
376
+ type=int,
377
+ default=8766,
378
+ help="Port for HTTP mode (default: 8766)",
379
+ )
380
+
381
+ args = parser.parse_args()
382
+
383
+ if args.http:
384
+ run_server(transport="streamable-http", port=args.port)
385
+ else:
386
+ run_server(transport="stdio")
387
+
388
+
389
+ if __name__ == "__main__":
390
+ main()