From 824a66d7c802d576938b8635006174b8aa6192a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claude4=CE=9Elope?= Date: Sat, 28 Jun 2025 15:28:32 +0200 Subject: [PATCH] Add tmux session management and screen to tmux conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Add tmux_server.py with full session management capabilities - Convert TheCoder script from screen to tmux with HOME bind mount fix - Add enabled flag to MCPServerConfig for selective server startup - Tmux now default, screen legacy (disabled by default) - Update documentation and architecture to reflect tmux preference Session Management: - create_session, execute, peek, list_sessions, kill_session - attach_session and share_session with multi-user instructions - Better multi-user support than screen with native tmux capabilities Testing: - Add test_tmux_session.py for comprehensive tmux functionality testing - Add test_screen_utf8.py for UTF-8 handling - Add MCP_QUICK_REFERENCE.md for AI handoff documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .tags | 75 ++++++- CLAUDE.md | 5 +- MCP_QUICK_REFERENCE.md | 111 ++++++++++ README.md | 5 +- docs/STRUCTURE.md | 4 + mcp_api_documentation.json | 165 +++++++++++++++ mcp_browser/config.py | 4 +- mcp_browser/multi_server.py | 13 +- mcp_servers/screen/tmux_server.py | 324 ++++++++++++++++++++++++++++++ test_screen_utf8.py | 101 ++++++++++ test_tmux_session.py | 155 ++++++++++++++ 11 files changed, 955 insertions(+), 7 deletions(-) create mode 100644 MCP_QUICK_REFERENCE.md create mode 100644 mcp_api_documentation.json create mode 100644 mcp_servers/screen/tmux_server.py create mode 100644 test_screen_utf8.py create mode 100644 test_tmux_session.py diff --git a/.tags b/.tags index 99b6ca1..e2839ee 100644 --- a/.tags +++ b/.tags @@ -18,6 +18,7 @@ DEFAULT_CONFIGS mcp_browser/default_configs.py /^DEFAULT_CONFIGS = {$/;" v Decision build/lib/mcp_servers/memory/memory_server.py /^class Decision:$/;" c Decision mcp_servers/memory/memory_server.py /^class Decision:$/;" c GenerateAIDocs setup.py /^class GenerateAIDocs(Command):$/;" c +InteractiveMCPClient mcp_browser/interactive_client.py /^class InteractiveMCPClient:$/;" c JSONRPCTestClient tests/test_integration.py /^class JSONRPCTestClient:$/;" c JsonRpcBuffer build/lib/mcp_browser/buffer.py /^class JsonRpcBuffer:$/;" c JsonRpcBuffer mcp_browser/buffer.py /^class JsonRpcBuffer:$/;" c @@ -55,7 +56,10 @@ TRACE build/lib/mcp_browser/logging_config.py /^TRACE = 5$/;" v TRACE mcp_browser/logging_config.py /^TRACE = 5$/;" v Task build/lib/mcp_servers/memory/memory_server.py /^class Task:$/;" c Task mcp_servers/memory/memory_server.py /^class Task:$/;" c +TestCmemIntegration tests/test_cmem_integration.py /^class TestCmemIntegration:$/;" c TestCommand setup.py /^class TestCommand(Command):$/;" c +TestInteractiveMCPClient tests/test_interactive_client.py /^class TestInteractiveMCPClient:$/;" c +TestInteractiveMCPClientIntegration tests/test_interactive_client.py /^class TestInteractiveMCPClientIntegration:$/;" c TestMCPBrowser tests/test_basic.py /^class TestMCPBrowser:$/;" c TestMessageFilter tests/test_basic.py /^class TestMessageFilter:$/;" c TestToolRegistry tests/test_basic.py /^class TestToolRegistry:$/;" c @@ -103,12 +107,13 @@ __init__ mcp_browser/daemon.py /^ def __init__(self, socket_path: Path):$/;" __init__ mcp_browser/default_configs.py /^ def __init__(self, config_dir: Optional[Path] = None):$/;" m class:ConfigManager __init__ mcp_browser/filter.py /^ def __init__(self, registry: ToolRegistry, server_callback: Callable):$/;" m class:VirtualToolHandler __init__ mcp_browser/filter.py /^ def __init__(self, registry: ToolRegistry, sparse_mode: bool = True):$/;" m class:MessageFilter +__init__ mcp_browser/interactive_client.py /^ def __init__(self, server_name: Optional[str] = None, use_daemon: bool = True):$/;" m class:InteractiveMCPClient __init__ mcp_browser/multi_server.py /^ def __init__(self, logger=None):$/;" m class:MultiServerManager __init__ mcp_browser/proxy.py /^ def __init__(self, config_path: Optional[Path] = None, server_name: Optional[str] = None,$/;" m class:MCPBrowser __init__ mcp_browser/registry.py /^ def __init__(self):$/;" m class:ToolRegistry __init__ mcp_browser/server.py /^ def __init__(self, config: MCPServerConfig, logger: Optional[logging.Logger] = None):$/;" m class:MCPServer __init__ mcp_servers/base.py /^ def __init__(self, name: str, version: str = "1.0.0"):$/;" m class:BaseMCPServer -__init__ mcp_servers/memory/memory_server.py /^ def __init__(self):$/;" m class:MemoryServer +__init__ mcp_servers/memory/memory_server.py /^ def __init__(self, identity: str = "default"):$/;" m class:MemoryServer __init__ mcp_servers/onboarding/onboarding_server.py /^ def __init__(self):$/;" m class:OnboardingServer __init__ mcp_servers/pattern_manager/pattern_server.py /^ def __init__(self):$/;" m class:PatternServer __init__ mcp_servers/screen/screen_server.py /^ def __init__(self):$/;" m class:ScreenServer @@ -119,20 +124,35 @@ __version__ build/lib/mcp_browser/__init__.py /^__version__ = "0.1.0"$/;" v __version__ mcp_browser/__init__.py /^__version__ = "0.1.0"$/;" v _add_pattern build/lib/mcp_servers/pattern_manager/pattern_server.py /^ async def _add_pattern(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:PatternServer _add_pattern mcp_servers/pattern_manager/pattern_server.py /^ async def _add_pattern(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:PatternServer +_add_user build/lib/mcp_servers/screen/screen_server.py /^ async def _add_user(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer +_add_user mcp_servers/screen/screen_server.py /^ async def _add_user(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer +_attach_multiuser build/lib/mcp_servers/screen/screen_server.py /^ async def _attach_multiuser(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer +_attach_multiuser mcp_servers/screen/screen_server.py /^ async def _attach_multiuser(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer _calculate_checksum build/lib/mcp_browser/default_configs.py /^ def _calculate_checksum(self, filepath: Path) -> str:$/;" m class:ConfigManager _calculate_checksum mcp_browser/default_configs.py /^ def _calculate_checksum(self, filepath: Path) -> str:$/;" m class:ConfigManager +_call_mcp mcp_browser/interactive_client.py /^ async def _call_mcp(self, request: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:InteractiveMCPClient +_call_tool mcp_browser/interactive_client.py /^ async def _call_tool(self, args: List[str]):$/;" f +_call_tool_direct mcp_browser/interactive_client.py /^ async def _call_tool_direct(self, tool_name: str, args: List[str]):$/;" f +_completer mcp_browser/interactive_client.py /^ def _completer(self, text: str, state: int) -> Optional[str]:$/;" m class:InteractiveMCPClient +_create_cmem_bridges mcp_servers/memory/memory_server.py /^ def _create_cmem_bridges(self, identity_dir: Path, session_dir: Path):$/;" m class:MemoryServer _create_session build/lib/mcp_servers/screen/screen_server.py /^ async def _create_session(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer _create_session mcp_servers/screen/screen_server.py /^ async def _create_session(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer _decision_add build/lib/mcp_servers/memory/memory_server.py /^ async def _decision_add(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:MemoryServer _decision_add mcp_servers/memory/memory_server.py /^ async def _decision_add(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:MemoryServer _delete_onboarding build/lib/mcp_servers/onboarding/onboarding_server.py /^ async def _delete_onboarding(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:OnboardingServer _delete_onboarding mcp_servers/onboarding/onboarding_server.py /^ async def _delete_onboarding(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:OnboardingServer +_discover_tools mcp_browser/interactive_client.py /^ async def _discover_tools(self, args: List[str]):$/;" f +_display_result mcp_browser/interactive_client.py /^ def _display_result(self, result: Any):$/;" f +_enable_multiuser build/lib/mcp_servers/screen/screen_server.py /^ async def _enable_multiuser(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer +_enable_multiuser mcp_servers/screen/screen_server.py /^ async def _enable_multiuser(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer _ensure_config_file build/lib/mcp_browser/default_configs.py /^ def _ensure_config_file(self, filename: str, config_data: Dict[str, str]) -> None:$/;" m class:ConfigManager _ensure_config_file mcp_browser/default_configs.py /^ def _ensure_config_file(self, filename: str, config_data: Dict[str, str]) -> None:$/;" m class:ConfigManager _execute_command build/lib/mcp_servers/screen/screen_server.py /^ async def _execute_command(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer +_execute_command mcp_browser/interactive_client.py /^ async def _execute_command(self, line: str):$/;" m class:InteractiveMCPClient _execute_command mcp_servers/screen/screen_server.py /^ async def _execute_command(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:ScreenServer _execute_pattern build/lib/mcp_servers/pattern_manager/pattern_server.py /^ async def _execute_pattern(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:PatternServer _execute_pattern mcp_servers/pattern_manager/pattern_server.py /^ async def _execute_pattern(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:PatternServer +_execute_tool_call mcp_browser/interactive_client.py /^ async def _execute_tool_call(self, tool_name: str, arguments: Dict[str, Any]):$/;" f _export_onboarding build/lib/mcp_servers/onboarding/onboarding_server.py /^ async def _export_onboarding(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:OnboardingServer _export_onboarding mcp_servers/onboarding/onboarding_server.py /^ async def _export_onboarding(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:OnboardingServer _filter_tools_response build/lib/mcp_browser/filter.py /^ def _filter_tools_response(self, message: dict) -> dict:$/;" m class:MessageFilter @@ -141,6 +161,7 @@ _format_onboarding build/lib/mcp_servers/onboarding/onboarding_server.py /^ d _format_onboarding mcp_servers/onboarding/onboarding_server.py /^ def _format_onboarding(self, identity: str, data: Dict[str, Any]) -> str:$/;" m class:OnboardingServer _forward_to_server build/lib/mcp_browser/proxy.py /^ async def _forward_to_server(self, request: dict) -> dict:$/;" m class:MCPBrowser _forward_to_server mcp_browser/proxy.py /^ async def _forward_to_server(self, request: dict) -> dict:$/;" m class:MCPBrowser +_generate_sample_args mcp_browser/interactive_client.py /^ def _generate_sample_args(self, schema: Dict[str, Any]) -> Dict[str, Any]:$/;" f _get_builtin_servers build/lib/mcp_browser/multi_server.py /^ def _get_builtin_servers(self) -> Dict[str, MCPServerConfig]:$/;" m class:MultiServerManager _get_builtin_servers mcp_browser/multi_server.py /^ def _get_builtin_servers(self) -> Dict[str, MCPServerConfig]:$/;" m class:MultiServerManager _handle_call build/lib/mcp_browser/filter.py /^ async def _handle_call(self, message: dict) -> dict:$/;" m class:VirtualToolHandler @@ -169,12 +190,14 @@ _list_patterns build/lib/mcp_servers/pattern_manager/pattern_server.py /^ asy _list_patterns mcp_servers/pattern_manager/pattern_server.py /^ async def _list_patterns(self) -> Dict[str, Any]:$/;" m class:PatternServer _list_sessions build/lib/mcp_servers/screen/screen_server.py /^ async def _list_sessions(self) -> Dict[str, Any]:$/;" m class:ScreenServer _list_sessions mcp_servers/screen/screen_server.py /^ async def _list_sessions(self) -> Dict[str, Any]:$/;" m class:ScreenServer +_list_tools mcp_browser/interactive_client.py /^ async def _list_tools(self, args: List[str]):$/;" f _load_json build/lib/mcp_servers/memory/memory_server.py /^ def _load_json(self, filename: str, default: Any) -> Any:$/;" m class:MemoryServer _load_json mcp_servers/memory/memory_server.py /^ def _load_json(self, filename: str, default: Any) -> Any:$/;" m class:MemoryServer _load_memory build/lib/mcp_servers/memory/memory_server.py /^ def _load_memory(self):$/;" m class:MemoryServer _load_memory mcp_servers/memory/memory_server.py /^ def _load_memory(self):$/;" m class:MemoryServer _load_patterns build/lib/mcp_servers/pattern_manager/pattern_server.py /^ def _load_patterns(self) -> Dict[str, Dict[str, Any]]:$/;" m class:PatternServer _load_patterns mcp_servers/pattern_manager/pattern_server.py /^ def _load_patterns(self) -> Dict[str, Dict[str, Any]]:$/;" m class:PatternServer +_manage_onboarding mcp_browser/interactive_client.py /^ async def _manage_onboarding(self, args: List[str]):$/;" f _mark_offline build/lib/mcp_browser/server.py /^ def _mark_offline(self):$/;" m class:MCPServer _mark_offline mcp_browser/server.py /^ def _mark_offline(self):$/;" m class:MCPServer _matches_pattern build/lib/mcp_servers/pattern_manager/pattern_server.py /^ def _matches_pattern(self, text: str, trigger: List[str]) -> bool:$/;" m class:PatternServer @@ -201,6 +224,7 @@ _read_stderr build/lib/mcp_browser/server.py /^ async def _read_stderr(self): _read_stderr mcp_browser/server.py /^ async def _read_stderr(self):$/;" m class:MCPServer _read_stdout build/lib/mcp_browser/server.py /^ async def _read_stdout(self):$/;" m class:MCPServer _read_stdout mcp_browser/server.py /^ async def _read_stdout(self):$/;" m class:MCPServer +_refresh_tools mcp_browser/interactive_client.py /^ async def _refresh_tools(self):$/;" m class:InteractiveMCPClient _register_tools build/lib/mcp_servers/memory/memory_server.py /^ def _register_tools(self):$/;" m class:MemoryServer _register_tools build/lib/mcp_servers/onboarding/onboarding_server.py /^ def _register_tools(self):$/;" m class:OnboardingServer _register_tools build/lib/mcp_servers/pattern_manager/pattern_server.py /^ def _register_tools(self):$/;" m class:PatternServer @@ -219,10 +243,17 @@ _save_json build/lib/mcp_servers/memory/memory_server.py /^ def _save_json(se _save_json mcp_servers/memory/memory_server.py /^ def _save_json(self, filename: str, data: Any):$/;" m class:MemoryServer _save_patterns build/lib/mcp_servers/pattern_manager/pattern_server.py /^ def _save_patterns(self):$/;" m class:PatternServer _save_patterns mcp_servers/pattern_manager/pattern_server.py /^ def _save_patterns(self):$/;" m class:PatternServer +_setup_cmem_integration mcp_servers/memory/memory_server.py /^ def _setup_cmem_integration(self) -> bool:$/;" m class:MemoryServer +_setup_readline mcp_browser/interactive_client.py /^ def _setup_readline(self):$/;" m class:InteractiveMCPClient +_show_help mcp_browser/interactive_client.py /^ def _show_help(self):$/;" m class:InteractiveMCPClient +_show_status mcp_browser/interactive_client.py /^ async def _show_status(self):$/;" f _signal_handler build/lib/mcp_browser/daemon.py /^ def _signal_handler(self, signum, frame):$/;" m class:MCPBrowserDaemon _signal_handler mcp_browser/daemon.py /^ def _signal_handler(self, signum, frame):$/;" m class:MCPBrowserDaemon _start_config_watcher build/lib/mcp_browser/proxy.py /^ async def _start_config_watcher(self):$/;" m class:MCPBrowser _start_config_watcher mcp_browser/proxy.py /^ async def _start_config_watcher(self):$/;" m class:MCPBrowser +_sync_decision_to_cmem mcp_servers/memory/memory_server.py /^ async def _sync_decision_to_cmem(self, decision: Decision):$/;" m class:MemoryServer +_sync_pattern_to_cmem mcp_servers/memory/memory_server.py /^ async def _sync_pattern_to_cmem(self, pattern: Pattern, action: str):$/;" m class:MemoryServer +_sync_task_to_cmem mcp_servers/memory/memory_server.py /^ async def _sync_task_to_cmem(self, task: Task, action: str):$/;" m class:MemoryServer _task_add build/lib/mcp_servers/memory/memory_server.py /^ async def _task_add(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:MemoryServer _task_add mcp_servers/memory/memory_server.py /^ async def _task_add(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:MemoryServer _task_list build/lib/mcp_servers/memory/memory_server.py /^ async def _task_list(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:MemoryServer @@ -231,6 +262,7 @@ _task_update build/lib/mcp_servers/memory/memory_server.py /^ async def _task _task_update mcp_servers/memory/memory_server.py /^ async def _task_update(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:MemoryServer _test_pattern build/lib/mcp_servers/pattern_manager/pattern_server.py /^ async def _test_pattern(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:PatternServer _test_pattern mcp_servers/pattern_manager/pattern_server.py /^ async def _test_pattern(self, args: Dict[str, Any]) -> Dict[str, Any]:$/;" m class:PatternServer +_test_tool mcp_browser/interactive_client.py /^ async def _test_tool(self, args: List[str]):$/;" f _update_server_configs build/lib/mcp_browser/proxy.py /^ def _update_server_configs(self):$/;" m class:MCPBrowser _update_server_configs mcp_browser/proxy.py /^ def _update_server_configs(self):$/;" m class:MCPBrowser add_message_handler build/lib/mcp_browser/server.py /^ def add_message_handler(self, handler: Callable[[dict], None]):$/;" m class:MCPServer @@ -258,6 +290,7 @@ call mcp_browser/proxy.py /^ async def call(self, jsonrpc_object: Dict[str, A call_tool build/lib/mcp_browser/__main__.py /^ call_tool = subparsers.add_parser("tools-call", help="Call a tool")$/;" v call_tool mcp_browser/__main__.py /^ call_tool = subparsers.add_parser("tools-call", help="Call a tool")$/;" v classifiers setup.py /^ classifiers=[$/;" v +cleanup mcp_browser/interactive_client.py /^ async def cleanup(self):$/;" f clear build/lib/mcp_browser/buffer.py /^ def clear(self):$/;" m class:JsonRpcBuffer clear mcp_browser/buffer.py /^ def clear(self):$/;" m class:JsonRpcBuffer close build/lib/mcp_browser/daemon.py /^ async def close(self):$/;" m class:MCPBrowserClient @@ -379,6 +412,7 @@ help mcp_browser/__main__.py /^ help="Test connection to s include_package_data setup.py /^ include_package_data=True,$/;" v initialize build/lib/mcp_browser/proxy.py /^ async def initialize(self):$/;" m class:MCPBrowser initialize examples/ai_optimized.py /^ async def initialize(self):$/;" m class:AIAssistant +initialize mcp_browser/interactive_client.py /^ async def initialize(self):$/;" m class:InteractiveMCPClient initialize mcp_browser/proxy.py /^ async def initialize(self):$/;" m class:MCPBrowser initialize_options setup.py /^ def initialize_options(self):$/;" m class:GenerateAIDocs initialize_options setup.py /^ def initialize_options(self):$/;" m class:TestCommand @@ -396,6 +430,7 @@ jsonrpc mcp_browser/__main__.py /^ jsonrpc = subparsers.add_parser("jsonrpc", keywords setup.py /^ keywords="mcp model-context-protocol ai llm tools json-rpc",$/;" v kill_daemon_with_children build/lib/mcp_browser/daemon.py /^def kill_daemon_with_children(socket_path: Path) -> bool:$/;" f kill_daemon_with_children mcp_browser/daemon.py /^def kill_daemon_with_children(socket_path: Path) -> bool:$/;" f +license setup.py /^ license="GPL-3.0-or-later",$/;" v load build/lib/mcp_browser/config.py /^ def load(self) -> MCPBrowserConfig:$/;" m class:ConfigLoader load mcp_browser/config.py /^ def load(self) -> MCPBrowserConfig:$/;" m class:ConfigLoader load_config build/lib/mcp_browser/default_configs.py /^ def load_config(self) -> dict:$/;" m class:ConfigManager @@ -414,11 +449,13 @@ main examples/basic_usage.py /^async def main():$/;" f main mcp_browser/__main__.py /^def main():$/;" f main mcp_browser/client_main.py /^def main():$/;" f main mcp_browser/daemon_main.py /^def main():$/;" f +main mcp_browser/interactive_client.py /^async def main():$/;" f main test_mcp_protocol.py /^async def main():$/;" f main tests/test_browser_functionality.py /^def main():$/;" f main tests/test_integration.py /^async def main():$/;" f mark_handled build/lib/mcp_browser/filter.py /^ def mark_handled(self, request_id: Union[str, int]):$/;" m class:MessageFilter mark_handled mcp_browser/filter.py /^ def mark_handled(self, request_id: Union[str, int]):$/;" m class:MessageFilter +mock_call tests/test_interactive_client.py /^ def mock_call(request):$/;" f function:TestInteractiveMCPClientIntegration.test_full_workflow_mock name setup.py /^ name="mcp-browser",$/;" v package_data setup.py /^ package_data={$/;" v packages setup.py /^ packages=find_packages(include=['mcp_browser*', 'mcp_servers*']),$/;" v @@ -437,6 +474,7 @@ route_tool_call mcp_browser/multi_server.py /^ async def route_tool_call(self run build/lib/mcp_browser/client_main.py /^ async def run():$/;" f function:main run build/lib/mcp_servers/base.py /^ async def run(self):$/;" m class:BaseMCPServer run mcp_browser/client_main.py /^ async def run():$/;" f function:main +run mcp_browser/interactive_client.py /^ async def run(self):$/;" m class:InteractiveMCPClient run mcp_servers/base.py /^ async def run(self):$/;" m class:BaseMCPServer run setup.py /^ def run(self):$/;" m class:GenerateAIDocs run setup.py /^ def run(self):$/;" m class:TestCommand @@ -475,6 +513,8 @@ set_onboarding_text build/lib/mcp_browser/default_configs.py /^ def set_onboa set_onboarding_text mcp_browser/default_configs.py /^ def set_onboarding_text(self, text: str, identity: Optional[str] = None) -> None:$/;" m class:ConfigManager setup_logging build/lib/mcp_browser/logging_config.py /^def setup_logging(debug: bool = False, log_file: Optional[Path] = None, $/;" f setup_logging mcp_browser/logging_config.py /^def setup_logging(debug: bool = False, log_file: Optional[Path] = None, $/;" f +setup_method tests/test_cmem_integration.py /^ def setup_method(self):$/;" m class:TestCmemIntegration +setup_method tests/test_interactive_client.py /^ def setup_method(self):$/;" m class:TestInteractiveMCPClient show_available_servers build/lib/mcp_browser/__main__.py /^def show_available_servers(config_path: Optional[str] = None):$/;" f show_available_servers mcp_browser/__main__.py /^def show_available_servers(config_path: Optional[str] = None):$/;" f show_configuration build/lib/mcp_browser/__main__.py /^def show_configuration(config_path: Optional[str] = None):$/;" f @@ -509,24 +549,57 @@ test_basic_flow tests/test_integration.py /^async def test_basic_flow():$/;" f test_brave_search_integration tests/test_brave_search.py /^async def test_brave_search_integration():$/;" f test_browser_without_servers tests/test_basic.py /^ async def test_browser_without_servers(self):$/;" m class:TestMCPBrowser test_browser_without_servers tests/test_browser_functionality.py /^async def test_browser_without_servers():$/;" f +test_call_mcp_browser tests/test_interactive_client.py /^ async def test_call_mcp_browser(self):$/;" m class:TestInteractiveMCPClient +test_call_mcp_client tests/test_interactive_client.py /^ async def test_call_mcp_client(self):$/;" m class:TestInteractiveMCPClient test_claude_connection tests/test_claude_connection.py /^async def test_claude_connection():$/;" f +test_completer_commands tests/test_interactive_client.py /^ def test_completer_commands(self):$/;" m class:TestInteractiveMCPClient +test_completer_tools tests/test_interactive_client.py /^ def test_completer_tools(self):$/;" m class:TestInteractiveMCPClient +test_create_cmem_bridges tests/test_cmem_integration.py /^ def test_create_cmem_bridges(self):$/;" m class:TestCmemIntegration test_daemon_initialization test_mcp_protocol.py /^async def test_daemon_initialization():$/;" f test_direct_initialization test_mcp_protocol.py /^async def test_direct_initialization():$/;" f test_discover_jsonpath tests/test_basic.py /^ def test_discover_jsonpath(self):$/;" m class:TestToolRegistry test_discovery test_discovery.py /^async def test_discovery():$/;" f +test_display_result_image_content tests/test_interactive_client.py /^ def test_display_result_image_content(self):$/;" m class:TestInteractiveMCPClient +test_display_result_raw_data tests/test_interactive_client.py /^ def test_display_result_raw_data(self):$/;" m class:TestInteractiveMCPClient +test_display_result_text_content tests/test_interactive_client.py /^ def test_display_result_text_content(self):$/;" m class:TestInteractiveMCPClient test_double_handshake_issue test_mcp_protocol.py /^async def test_double_handshake_issue():$/;" f test_duplicate_error_filtering tests/test_basic.py /^ def test_duplicate_error_filtering(self):$/;" m class:TestMessageFilter test_error_handling tests/test_integration.py /^async def test_error_handling():$/;" f +test_execute_command_help tests/test_interactive_client.py /^ async def test_execute_command_help(self):$/;" m class:TestInteractiveMCPClient +test_execute_command_list tests/test_interactive_client.py /^ async def test_execute_command_list(self):$/;" m class:TestInteractiveMCPClient +test_execute_command_refresh tests/test_interactive_client.py /^ async def test_execute_command_refresh(self):$/;" m class:TestInteractiveMCPClient +test_execute_command_unknown_tool tests/test_interactive_client.py /^ async def test_execute_command_unknown_tool(self):$/;" m class:TestInteractiveMCPClient +test_execute_tool_call tests/test_interactive_client.py /^ async def test_execute_tool_call(self):$/;" m class:TestInteractiveMCPClient +test_execute_tool_call_error tests/test_interactive_client.py /^ async def test_execute_tool_call_error(self):$/;" m class:TestInteractiveMCPClient +test_full_workflow_mock tests/test_interactive_client.py /^ async def test_full_workflow_mock(self):$/;" m class:TestInteractiveMCPClientIntegration +test_generate_sample_args_examples tests/test_interactive_client.py /^ def test_generate_sample_args_examples(self):$/;" m class:TestInteractiveMCPClient +test_generate_sample_args_string tests/test_interactive_client.py /^ def test_generate_sample_args_string(self):$/;" m class:TestInteractiveMCPClient +test_generate_sample_args_types tests/test_interactive_client.py /^ def test_generate_sample_args_types(self):$/;" m class:TestInteractiveMCPClient +test_initialization tests/test_interactive_client.py /^ def test_initialization(self):$/;" m class:TestInteractiveMCPClient +test_memory_server_initialization_custom_identity tests/test_cmem_integration.py /^ def test_memory_server_initialization_custom_identity(self):$/;" m class:TestCmemIntegration +test_memory_server_initialization_default tests/test_cmem_integration.py /^ def test_memory_server_initialization_default(self):$/;" m class:TestCmemIntegration test_message_filter tests/test_simple.py /^def test_message_filter():$/;" f test_onboarding tests/test_onboarding.py /^async def test_onboarding():$/;" f +test_refresh_tools tests/test_interactive_client.py /^ async def test_refresh_tools(self):$/;" m class:TestInteractiveMCPClient +test_screen_multiuser test_screen_multiuser.py /^async def test_screen_multiuser():$/;" f test_screen_utf8 test_screen_utf8.py /^async def test_screen_utf8():$/;" f test_server_connection build/lib/mcp_browser/__main__.py /^async def test_server_connection(browser: MCPBrowser, server_name: Optional[str] = None):$/;" f test_server_connection mcp_browser/__main__.py /^async def test_server_connection(browser: MCPBrowser, server_name: Optional[str] = None):$/;" f test_server_mode_initialization test_mcp_protocol.py /^async def test_server_mode_initialization():$/;" f test_server_mode_timeout test_claude_desktop_flow.py /^async def test_server_mode_timeout():$/;" f +test_setup_cmem_integration_available tests/test_cmem_integration.py /^ def test_setup_cmem_integration_available(self):$/;" m class:TestCmemIntegration +test_setup_cmem_integration_no_cmem tests/test_cmem_integration.py /^ def test_setup_cmem_integration_no_cmem(self):$/;" m class:TestCmemIntegration test_sparse_mode tests/test_simple.py /^def test_sparse_mode():$/;" f test_sparse_mode_filtering tests/test_basic.py /^ def test_sparse_mode_filtering(self):$/;" m class:TestMessageFilter test_sparse_tools tests/test_basic.py /^ def test_sparse_tools(self):$/;" m class:TestToolRegistry +test_sync_decision_to_cmem tests/test_cmem_integration.py /^ async def test_sync_decision_to_cmem(self):$/;" m class:TestCmemIntegration +test_sync_error_handling tests/test_cmem_integration.py /^ async def test_sync_error_handling(self):$/;" m class:TestCmemIntegration +test_sync_pattern_to_cmem tests/test_cmem_integration.py /^ async def test_sync_pattern_to_cmem(self):$/;" m class:TestCmemIntegration +test_sync_task_to_cmem_add tests/test_cmem_integration.py /^ async def test_sync_task_to_cmem_add(self):$/;" m class:TestCmemIntegration +test_sync_task_to_cmem_complete tests/test_cmem_integration.py /^ async def test_sync_task_to_cmem_complete(self):$/;" m class:TestCmemIntegration +test_sync_with_integration_disabled tests/test_cmem_integration.py /^ async def test_sync_with_integration_disabled(self):$/;" m class:TestCmemIntegration +test_task_add_with_sync tests/test_cmem_integration.py /^ async def test_task_add_with_sync(self):$/;" m class:TestCmemIntegration +test_task_update_completion_with_sync tests/test_cmem_integration.py /^ async def test_task_update_completion_with_sync(self):$/;" m class:TestCmemIntegration test_tool_registry tests/test_simple.py /^def test_tool_registry():$/;" f test_update_tools tests/test_basic.py /^ def test_update_tools(self):$/;" m class:TestToolRegistry test_virtual_tool_handling tests/test_basic.py /^ async def test_virtual_tool_handling(self):$/;" m class:TestMCPBrowser diff --git a/CLAUDE.md b/CLAUDE.md index b5ab00b..cf58d8c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,8 +40,9 @@ python setup.py aidocs 1. **Virtual Tools**: `mcp_discover`, `mcp_call`, and `onboarding` exist only in the browser layer 2. **Tool Namespacing**: Format is `server::tool` or `mcp__namespace__tool` -3. **Multi-Server**: Built-in servers (screen, memory, patterns, onboarding) start automatically +3. **Multi-Server**: Built-in servers (tmux, memory, patterns, onboarding) start automatically 4. **Identity-Aware**: Onboarding tool accepts identity parameter for context-specific instructions +5. **Session Management**: Tmux preferred over screen for better multi-user support ### Architecture Overview @@ -55,7 +56,7 @@ mcp_browser/ mcp_servers/ ├── base.py # Base class for Python MCP servers -├── screen/ # GNU screen session management +├── screen/ # Session management (tmux preferred, screen legacy) ├── memory/ # Persistent memory and tasks ├── patterns/ # Auto-response patterns └── onboarding/ # Identity-aware onboarding diff --git a/MCP_QUICK_REFERENCE.md b/MCP_QUICK_REFERENCE.md new file mode 100644 index 0000000..f759a6b --- /dev/null +++ b/MCP_QUICK_REFERENCE.md @@ -0,0 +1,111 @@ +# MCP Quick Reference for Fresh Claude Instances + +## 🔗 MCP Interface Access + +**Status**: ✅ mcp-browser is configured and running +- **Claude Desktop** → **mcp-browser** → **27 tools from 7 servers** +- **Config**: `/home/claude/.claude/mcp-browser/config.yaml` +- **Tools**: Built-in servers (screen, memory, patterns, onboarding) + external (claude-code, brave-search, filesystem, github) + +## 🚀 Quick MCP Commands + +### Discovery (Context-Safe) +```python +# List all available tool names (27 total) +mcp_discover(jsonpath="$.tools[*].name") + +# Find memory-related tools (regex support) +mcp_discover(jsonpath="$.tools[?(@.name =~ /memory|task|pattern/i)]") + +# Get server information +mcp_discover(jsonpath="$.servers[*].name") + +# Get claude-code tools specifically +mcp_discover(jsonpath="$.servers['claude-code'].tools[*].name") +``` + +### Tool Execution +```python +# Call any discovered tool +mcp_call( + method="tools/call", + params={ + "name": "task_list", + "arguments": {"status": "pending"} + } +) + +# Call claude-code tools +mcp_call( + method="tools/call", + params={ + "name": "claude-code::read_file", + "arguments": {"path": "/path/to/file.py"} + } +) +``` + +## 🧠 Memory & Handoff System + +### Get Current Context +```bash +cmem handoff # Quick context summary +cmem task list # Active tasks +cmem pattern list # Available patterns +``` + +### Essential Memory Tools (via MCP) +- `task_add` - Add new tasks with priority/assignee +- `task_update` - Update task status +- `memory_summary` - Get project overview +- `knowledge_add` - Store information +- `pattern_add` - Record learning patterns + +## 🛠️ Built-in MCP Tools Available + +**Screen Management (8 tools)**: +- `create_session`, `execute`, `peek`, `list_sessions`, `kill_session` + +**Memory & Tasks (10 tools)**: +- `task_add`, `task_list`, `task_update`, `decision_add`, `pattern_add` + +**Auto-Response Patterns (5 tools)**: +- `add_pattern`, `list_patterns`, `test_pattern` + +**Identity & Onboarding (4 tools)**: +- `onboarding`, `onboarding_list`, `onboarding_export` + +## 🎯 Current Project Context + +**Location**: `/mnt/data/claude/claude` (bind mounted from `/home/claude`) +**Active Projects**: +- `mcp-browser` (✅ Working, integrated) +- `xilope` (🔄 In development - XDG config system needed) + +**Memory Storage**: `/mnt/data/claude/claude/.mcp-memory/` +**cmem Wrapper**: `/mnt/data/claude/claude/bin/cmem` → `/usr/local/bin/cmem` + +## 🔧 For Fresh Claude Instances + +1. **Read this file** for MCP context +2. **Run `cmem handoff`** for session continuity +3. **Use `mcp_discover`** to explore available tools +4. **Check `CLAUDE.md`** for project-specific instructions +5. **Access Xilope onboarding**: `onboarding(identity="xilope_production")` + +## 📚 Documentation Locations + +- **Handoff Guide**: `/home/claude/claude-utils/mcp-browser/HANDOFF_INSTRUCTIONS.md` +- **Xilope Production**: `/home/claude/claude-utils/mcp-browser/mcp_servers/onboarding/xilope_production.md` +- **MCP Config**: `/home/claude/.claude/mcp-browser/config.yaml` +- **Memory Files**: `/mnt/data/claude/claude/.mcp-memory/default/` + +## ⚡ Generate Complete API Docs + +```bash +cd /home/claude/claude-utils/mcp-browser +python setup.py gen_apidoc +# Creates: mcp_api_documentation.json (27 tools, 7 servers documented) +``` + +This provides **complete MCP ecosystem access** with persistent memory across Claude sessions. \ No newline at end of file diff --git a/README.md b/README.md index d5ddab0..e92e349 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ MCP Browser acts as a smart proxy between AI systems and MCP servers, providing: - Configuration-driven server management 4. **Built-in Servers** - - **Screen**: GNU screen session management for persistent processes + - **Tmux**: tmux session management for persistent processes (default) + - **Screen**: GNU screen session management (legacy, disabled by default) - **Memory**: Project memory, tasks, decisions, and knowledge management - **Patterns**: Auto-response pattern management for automation - **Onboarding**: Identity-aware onboarding for AI contexts @@ -53,7 +54,7 @@ mcp-browser/ │ └── config.py # Configuration management ├── mcp_servers/ # Built-in MCP servers │ ├── base.py # Base server implementation -│ ├── screen/ # Screen session management +│ ├── screen/ # Session management (tmux + screen) │ ├── memory/ # Memory and context management │ ├── pattern_manager/ # Pattern automation │ └── onboarding/ # Identity-aware onboarding diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md index fe0c072..be1433f 100644 --- a/docs/STRUCTURE.md +++ b/docs/STRUCTURE.md @@ -6,6 +6,7 @@ test_claude_desktop_flow.py test_discovery.py test_mcp_protocol.py + test_screen_multiuser.py test_screen_utf8.py mcp_browser.egg-info/ systemd/ @@ -55,7 +56,9 @@ test_brave_search.py test_browser_functionality.py test_claude_connection.py + test_cmem_integration.py test_integration.py + test_interactive_client.py test_onboarding.py test_simple.py config/ @@ -84,6 +87,7 @@ daemon_main.py default_configs.py filter.py + interactive_client.py logging_config.py multi_server.py proxy.py diff --git a/mcp_api_documentation.json b/mcp_api_documentation.json new file mode 100644 index 0000000..24db044 --- /dev/null +++ b/mcp_api_documentation.json @@ -0,0 +1,165 @@ +{ + "mcp_browser_version": "0.2.0", + "total_servers": 7, + "total_tools": 27, + "generation_timestamp": "2025-06-28T13:16:38.436458", + "servers": { + "default": { + "name": "default", + "description": "Default in-memory MCP server", + "command": [ + "npx", + "-y", + "@modelcontextprotocol/server-memory" + ], + "status": "configured", + "tools": [], + "tool_count": 0, + "tool_names": [], + "environment": {}, + "working_directory": null, + "capabilities": [] + }, + "claude-code": { + "name": "claude-code", + "description": "MCP interface to claude code started in /home/claude", + "command": [ + "/home/claude/bin/claude" + ], + "status": "configured", + "tools": [], + "tool_count": 0, + "tool_names": [], + "environment": {}, + "working_directory": null, + "capabilities": [] + }, + "brave-search": { + "name": "brave-search", + "description": "Brave Search API access", + "command": [ + "npx", + "-y", + "@modelcontextprotocol/server-brave-search" + ], + "status": "configured", + "tools": [], + "tool_count": 0, + "tool_names": [], + "environment": {}, + "working_directory": null, + "capabilities": [] + }, + "filesystem": { + "name": "filesystem", + "description": "File system access (/home/claude)", + "command": [ + "npx", + "-y", + "@modelcontextprotocol/server-filesystem", + "/home/claude" + ], + "status": "configured", + "tools": [], + "tool_count": 0, + "tool_names": [], + "environment": {}, + "working_directory": null, + "capabilities": [] + }, + "github": { + "name": "github", + "description": "GitHub API access", + "command": [ + "npx", + "-y", + "@modelcontextprotocol/server-github" + ], + "status": "configured", + "tools": [], + "tool_count": 0, + "tool_names": [], + "environment": {}, + "working_directory": null, + "capabilities": [] + }, + "memory": { + "name": "memory", + "description": "Persistent memory and notes", + "command": [ + "npx", + "-y", + "@modelcontextprotocol/server-memory" + ], + "status": "configured", + "tools": [], + "tool_count": 0, + "tool_names": [], + "environment": {}, + "working_directory": null, + "capabilities": [] + }, + "builtin-only": { + "name": "builtin-only", + "description": "Use only built-in Python servers", + "command": null, + "status": "configured", + "tools": [], + "tool_count": 0, + "tool_names": [], + "environment": {}, + "working_directory": null, + "capabilities": [] + } + }, + "builtin": { + "name": "builtin", + "description": "Built-in MCP Browser servers (screen, memory, patterns, onboarding)", + "status": "active", + "tools": [], + "tool_count": 0, + "capabilities": [ + "screen_management", + "memory_storage", + "pattern_matching", + "onboarding_management" + ] + }, + "discovery_patterns": { + "all_tools": "$.tools[*]", + "all_tool_names": "$.tools[*].name", + "tools_by_server": "$.servers[*].tools[*]", + "tool_schemas": "$.tools[*].inputSchema", + "memory_tools": "$.tools[?(@.name =~ /memory|task|pattern|knowledge/i)]", + "screen_tools": "$.tools[?(@.name =~ /screen|session/i)]", + "find_tool_by_name": "$.tools[?(@.name=='TOOL_NAME')]", + "server_capabilities": "$.servers[*].capabilities" + }, + "sparse_mode_info": { + "visible_tools": [ + "mcp_discover", + "mcp_call", + "onboarding" + ], + "hidden_tools": 27, + "purpose": "Context optimization - full MCP API accessible via proxy tools" + }, + "runtime_status": { + "builtin:screen": { + "status": "active", + "pid": 589886 + }, + "builtin:memory": { + "status": "active", + "pid": 589890 + }, + "builtin:patterns": { + "status": "active", + "pid": 589905 + }, + "builtin:onboarding": { + "status": "active", + "pid": 589908 + } + } +} \ No newline at end of file diff --git a/mcp_browser/config.py b/mcp_browser/config.py index 2cfdb94..38ad328 100644 --- a/mcp_browser/config.py +++ b/mcp_browser/config.py @@ -22,6 +22,7 @@ class MCPServerConfig: env: Dict[str, str] = field(default_factory=dict) name: Optional[str] = None description: Optional[str] = None + enabled: bool = True @dataclass @@ -88,7 +89,8 @@ class ConfigLoader: args=server_config.get("args", []), env=server_config.get("env", {}), name=server_config.get("name", name), - description=server_config.get("description") + description=server_config.get("description"), + enabled=server_config.get("enabled", True) ) self._config = MCPBrowserConfig( diff --git a/mcp_browser/multi_server.py b/mcp_browser/multi_server.py index aee399d..eb4df08 100644 --- a/mcp_browser/multi_server.py +++ b/mcp_browser/multi_server.py @@ -28,10 +28,16 @@ class MultiServerManager: base_path = Path(__file__).parent.parent / "mcp_servers" return { + "builtin:tmux": MCPServerConfig( + command=["python3", str(base_path / "screen" / "tmux_server.py")], + name="tmux", + description="tmux session management" + ), "builtin:screen": MCPServerConfig( command=["python3", str(base_path / "screen" / "screen_server.py")], name="screen", - description="GNU screen session management" + description="GNU screen session management (legacy)", + enabled=False # Disabled by default, tmux is preferred ), "builtin:memory": MCPServerConfig( command=["python3", str(base_path / "memory" / "memory_server.py")], @@ -53,6 +59,11 @@ class MultiServerManager: async def start_builtin_servers(self): """Start all built-in servers.""" for name, config in self.builtin_servers.items(): + # Skip disabled servers + if not config.enabled: + self.logger.info(f"Skipping disabled built-in server: {name}") + continue + self.logger.info(f"Starting built-in server: {name}") server = MCPServer(config, logger=get_logger(__name__, name)) diff --git a/mcp_servers/screen/tmux_server.py b/mcp_servers/screen/tmux_server.py new file mode 100644 index 0000000..030adb7 --- /dev/null +++ b/mcp_servers/screen/tmux_server.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +""" +Tmux MCP Server - tmux session management. + +Provides tools for creating and managing persistent tmux sessions, +useful for long-running processes and maintaining shell state. +""" + +import os +import sys +import asyncio +import subprocess +import json +from typing import Dict, Any, List, Optional +from pathlib import Path + +# Add parent directory to path +sys.path.append(str(Path(__file__).parent.parent)) +from base import BaseMCPServer + + +class TmuxServer(BaseMCPServer): + """MCP server for tmux management.""" + + def __init__(self): + super().__init__("tmux-server", "1.0.0") + self._register_tools() + + def _register_tools(self): + """Register all tmux management tools.""" + + # Create session tool + self.register_tool( + name="create_session", + description="Create a new tmux session with optional initial command", + input_schema={ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name for the tmux session" + }, + "command": { + "type": "string", + "description": "Optional command to run in the session" + } + }, + "required": ["name"] + } + ) + + # Execute command tool + self.register_tool( + name="execute", + description="Execute a command in an existing tmux session", + input_schema={ + "type": "object", + "properties": { + "session": { + "type": "string", + "description": "Name of the tmux session" + }, + "command": { + "type": "string", + "description": "Command to execute" + } + }, + "required": ["session", "command"] + } + ) + + # Peek at session output + self.register_tool( + name="peek", + description="Get recent output from a tmux session (last 50 lines by default)", + input_schema={ + "type": "object", + "properties": { + "session": { + "type": "string", + "description": "Name of the tmux session" + }, + "lines": { + "type": "integer", + "description": "Number of lines to retrieve (default: 50)", + "default": 50 + } + }, + "required": ["session"] + } + ) + + # List sessions + self.register_tool( + name="list_sessions", + description="List all active tmux sessions", + input_schema={ + "type": "object", + "properties": {} + } + ) + + # Kill session + self.register_tool( + name="kill_session", + description="Terminate a tmux session", + input_schema={ + "type": "object", + "properties": { + "session": { + "type": "string", + "description": "Name of the tmux session to kill" + } + }, + "required": ["session"] + } + ) + + # Attach to session (provides instructions) + self.register_tool( + name="attach_session", + description="Provide instructions for attaching to a tmux session", + input_schema={ + "type": "object", + "properties": { + "session": { + "type": "string", + "description": "Name of the tmux session" + } + }, + "required": ["session"] + } + ) + + # Share session (tmux supports multiple clients by default) + self.register_tool( + name="share_session", + description="Get instructions for sharing a tmux session with other users", + input_schema={ + "type": "object", + "properties": { + "session": { + "type": "string", + "description": "Name of the tmux session" + } + }, + "required": ["session"] + } + ) + + async def handle_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: + """Handle tmux tool calls.""" + + if tool_name == "create_session": + return await self._create_session(arguments) + elif tool_name == "execute": + return await self._execute_command(arguments) + elif tool_name == "peek": + return await self._peek_session(arguments) + elif tool_name == "list_sessions": + return await self._list_sessions() + elif tool_name == "kill_session": + return await self._kill_session(arguments) + elif tool_name == "attach_session": + return await self._attach_session(arguments) + elif tool_name == "share_session": + return await self._share_session(arguments) + else: + raise Exception(f"Unknown tool: {tool_name}") + + async def _create_session(self, args: Dict[str, Any]) -> Dict[str, Any]: + """Create a new tmux session.""" + name = args["name"] + command = args.get("command") + + # Check if session already exists + check_result = await self._run_command(["tmux", "list-sessions", "-F", "#{session_name}"]) + if check_result.returncode == 0 and name in check_result.stdout.split('\n'): + return self.content_text(f"Session '{name}' already exists") + + # Create session + cmd = ["tmux", "new-session", "-d", "-s", name] + if command: + cmd.append(command) + + result = await self._run_command(cmd) + + if result.returncode == 0: + return self.content_text(f"Created tmux session '{name}'" + + (f" running '{command}'" if command else "")) + else: + return self.content_text(f"Failed to create session: {result.stderr}") + + async def _execute_command(self, args: Dict[str, Any]) -> Dict[str, Any]: + """Execute a command in a tmux session.""" + session = args["session"] + command = args["command"] + + # Send command to tmux session + cmd = ["tmux", "send-keys", "-t", session, command, "Enter"] + + result = await self._run_command(cmd) + + if result.returncode == 0: + return self.content_text(f"Executed command in session '{session}'") + else: + return self.content_text(f"Failed to execute command: {result.stderr}") + + async def _peek_session(self, args: Dict[str, Any]) -> Dict[str, Any]: + """Get recent output from a tmux session.""" + session = args["session"] + lines = args.get("lines", 50) + + # Get pane content from tmux + cmd = ["tmux", "capture-pane", "-t", session, "-p"] + + result = await self._run_command(cmd) + + if result.returncode != 0: + return self.content_text(f"Failed to peek at session: {result.stderr}") + + # Clean ANSI escape sequences + import re + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + content = ansi_escape.sub('', result.stdout) + + # Get last N lines + output_lines = content.strip().split('\n') + if len(output_lines) > lines: + output_lines = output_lines[-lines:] + + output = '\n'.join(output_lines) + + return self.content_text(output if output else "(No output)") + + async def _list_sessions(self) -> Dict[str, Any]: + """List all active tmux sessions.""" + result = await self._run_command(["tmux", "list-sessions", "-F", "#{session_name}: #{?session_attached,attached,not attached} (#{session_windows} windows)"]) + + if result.returncode != 0: + if "no server running" in result.stderr.lower(): + return self.content_text("No tmux server running (no active sessions)") + else: + return self.content_text(f"Error listing sessions: {result.stderr}") + + if result.stdout.strip(): + output = "Active tmux sessions:\n" + result.stdout.strip() + else: + output = "No active tmux sessions" + + return self.content_text(output) + + async def _kill_session(self, args: Dict[str, Any]) -> Dict[str, Any]: + """Kill a tmux session.""" + session = args["session"] + + cmd = ["tmux", "kill-session", "-t", session] + result = await self._run_command(cmd) + + if result.returncode == 0: + return self.content_text(f"Killed tmux session '{session}'") + else: + return self.content_text(f"Failed to kill session: {result.stderr}") + + async def _attach_session(self, args: Dict[str, Any]) -> Dict[str, Any]: + """Provide instructions for attaching to a tmux session.""" + session = args["session"] + + # Check if session exists + check_result = await self._run_command(["tmux", "list-sessions", "-F", "#{session_name}"]) + if check_result.returncode != 0 or session not in check_result.stdout.split('\n'): + return self.content_text(f"Session '{session}' not found") + + # Provide attach command + attach_cmd = f"tmux attach-session -t {session}" + + return self.content_text(f"To attach to session '{session}', run: {attach_cmd}") + + async def _share_session(self, args: Dict[str, Any]) -> Dict[str, Any]: + """Get instructions for sharing a tmux session.""" + session = args["session"] + + # Check if session exists + check_result = await self._run_command(["tmux", "list-sessions", "-F", "#{session_name}"]) + if check_result.returncode != 0 or session not in check_result.stdout.split('\n'): + return self.content_text(f"Session '{session}' not found") + + instructions = f"""To share tmux session '{session}': + +1. Multiple users can attach simultaneously: + tmux attach-session -t {session} + +2. For read-only access: + tmux attach-session -t {session} -r + +3. To create a new session attached to the same windows: + tmux new-session -t {session} + +Note: tmux supports multiple clients by default - no special setup needed!""" + + return self.content_text(instructions) + + async def _run_command(self, cmd: List[str]) -> subprocess.CompletedProcess: + """Run a command and return the result.""" + return await asyncio.to_thread( + subprocess.run, + cmd, + capture_output=True, + text=True + ) + + +if __name__ == "__main__": + # Check if tmux is installed + try: + subprocess.run(["tmux", "-V"], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print("Error: tmux is not installed", file=sys.stderr) + print("Install it with: sudo apt-get install tmux", file=sys.stderr) + sys.exit(1) + + # Run the server + server = TmuxServer() + asyncio.run(server.run()) \ No newline at end of file diff --git a/test_screen_utf8.py b/test_screen_utf8.py new file mode 100644 index 0000000..560b883 --- /dev/null +++ b/test_screen_utf8.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Test UTF-8 handling in screen peek functionality.""" + +import asyncio +import json +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from mcp_browser import MCPBrowser + + +async def test_screen_utf8(): + """Test screen peek with non-UTF8 content.""" + browser = MCPBrowser(enable_builtin_servers=True) + + await browser.initialize() + + print("=== Testing Screen UTF-8 Handling ===\n") + + # Create a test session + print("1. Creating test session...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "create_session", + "arguments": { + "name": "utf8-test", + "command": "bash" + } + } + }) + print(f" Result: {response.get('result', {}).get('content', [{}])[0].get('text', 'Error')}") + + # Execute command that produces mixed encoding + print("\n2. Executing command with mixed encoding...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "execute", + "arguments": { + "session": "utf8-test", + "command": "echo -e 'UTF-8: café\\nBinary: \\x80\\x81\\x82\\nEmoji: 🤖'" + } + } + }) + print(f" Result: {response.get('result', {}).get('content', [{}])[0].get('text', 'Error')}") + + # Wait a moment for output + await asyncio.sleep(0.5) + + # Peek at the session + print("\n3. Peeking at session output...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "peek", + "arguments": { + "session": "utf8-test", + "lines": 10 + } + } + }) + + if "result" in response: + output = response["result"]["content"][0]["text"] + print(f" Output:\n{output}") + + # Check if we handled the encoding properly + if "café" in output and "🤖" in output: + print("\n✓ UTF-8 encoding handled correctly!") + else: + print("\n⚠ Some UTF-8 characters may not have been decoded properly") + else: + print(f" Error: {response}") + + # Clean up + print("\n4. Cleaning up...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "kill_session", + "arguments": {"session": "utf8-test"} + } + }) + print(f" Result: {response.get('result', {}).get('content', [{}])[0].get('text', 'Error')}") + + await browser.close() + + +if __name__ == "__main__": + asyncio.run(test_screen_utf8()) \ No newline at end of file diff --git a/test_tmux_session.py b/test_tmux_session.py new file mode 100644 index 0000000..70b3d83 --- /dev/null +++ b/test_tmux_session.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Test tmux session functionality.""" + +import asyncio +import json +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from mcp_browser import MCPBrowser + + +async def test_tmux_session(): + """Test tmux session functionality.""" + browser = MCPBrowser(enable_builtin_servers=True) + + await browser.initialize() + + print("=== Testing Tmux Session Functionality ===\n") + + # Create a test session + print("1. Creating test session...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "builtin:tmux::create_session", + "arguments": { + "name": "tmux-test", + "command": "bash" + } + } + }) + print(f" Result: {response.get('result', {}).get('content', [{}])[0].get('text', 'Error')}") + + # Execute a command in the session + print("\n2. Executing command in session...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "builtin:tmux::execute", + "arguments": { + "session": "tmux-test", + "command": "echo 'Tmux session test - Hello World!'" + } + } + }) + print(f" Result: {response.get('result', {}).get('content', [{}])[0].get('text', 'Error')}") + + # Wait a moment for output + await asyncio.sleep(0.5) + + # Test peek functionality + print("\n3. Testing peek functionality...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "builtin:tmux::peek", + "arguments": { + "session": "tmux-test", + "lines": 10 + } + } + }) + + if "result" in response: + output = response["result"]["content"][0]["text"] + print(f" Output:\n{output}") + + # Check if we can see the executed command output + if "Tmux session test" in output: + print("\n✓ Peek works correctly!") + else: + print("\n⚠ Peek output might not show recent commands") + else: + print(f" Error: {response}") + + # List sessions + print("\n4. Listing sessions...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "builtin:tmux::list_sessions", + "arguments": {} + } + }) + + if "result" in response: + sessions_output = response["result"]["content"][0]["text"] + print(f" Sessions:\n{sessions_output}") + + # Check if session is listed + if "tmux-test" in sessions_output: + print("\n✓ Tmux session is listed!") + else: + print("\n⚠ Session not found in list") + else: + print(f" Error: {response}") + + # Get attach instructions + print("\n5. Getting attach instructions...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "builtin:tmux::attach_session", + "arguments": { + "session": "tmux-test" + } + } + }) + print(f" Result: {response.get('result', {}).get('content', [{}])[0].get('text', 'Error')}") + + # Test share instructions + print("\n6. Getting share instructions...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 6, + "method": "tools/call", + "params": { + "name": "builtin:tmux::share_session", + "arguments": { + "session": "tmux-test" + } + } + }) + print(f" Result: {response.get('result', {}).get('content', [{}])[0].get('text', 'Error')}") + + # Clean up + print("\n7. Cleaning up...") + response = await browser.call({ + "jsonrpc": "2.0", + "id": 7, + "method": "tools/call", + "params": { + "name": "builtin:tmux::kill_session", + "arguments": {"session": "tmux-test"} + } + }) + print(f" Result: {response.get('result', {}).get('content', [{}])[0].get('text', 'Error')}") + + await browser.close() + + +if __name__ == "__main__": + asyncio.run(test_tmux_session()) \ No newline at end of file