#!/usr/bin/env python3 """ Enhanced Interactive MCP Browser Client Provides a user-friendly interactive interface for exploring and using MCP tools with better discovery, autocompletion, and testing capabilities. """ import asyncio import json import readline import sys from pathlib import Path from typing import Dict, Any, List, Optional import traceback from .proxy import MCPBrowser from .daemon import MCPBrowserClient, get_socket_path, is_daemon_running from .logging_config import get_logger class InteractiveMCPClient: """Enhanced interactive MCP browser client.""" def __init__(self, server_name: Optional[str] = None, use_daemon: bool = True): self.server_name = server_name self.use_daemon = use_daemon self.browser: Optional[MCPBrowser] = None self.client: Optional[MCPBrowserClient] = None self.logger = get_logger(__name__) self.tool_cache: Dict[str, Any] = {} self.command_history: List[str] = [] # Setup readline self._setup_readline() def _setup_readline(self): """Setup readline for better command line experience.""" readline.set_completer(self._completer) readline.parse_and_bind('tab: complete') readline.set_completer_delims(' \t\n`!@#$%^&*()=+[{]}\\|;:\'",<>?') # Load history history_file = Path.home() / ".mcp_browser_history" try: readline.read_history_file(str(history_file)) except FileNotFoundError: pass # Save history on exit import atexit atexit.register(readline.write_history_file, str(history_file)) def _completer(self, text: str, state: int) -> Optional[str]: """Tab completion for commands and tool names.""" if state == 0: # Get current line line = readline.get_line_buffer() # Complete commands commands = ['discover', 'call', 'list', 'help', 'quit', 'onboard', 'status', 'test'] # Add tool names if we have them cached if self.tool_cache: commands.extend(self.tool_cache.keys()) # Filter matches self.matches = [cmd for cmd in commands if cmd.startswith(text)] try: return self.matches[state] except IndexError: return None async def initialize(self): """Initialize the MCP browser connection.""" print("๐Ÿ” MCP Browser Interactive Mode") print("Type 'help' for commands, 'quit' to exit") print() # Try to connect if self.use_daemon: socket_path = get_socket_path(self.server_name) if is_daemon_running(socket_path): try: self.client = MCPBrowserClient(socket_path) await self.client.__aenter__() print(f"โœ… Connected to daemon at {socket_path}") except Exception as e: self.logger.warning(f"Failed to connect to daemon: {e}") self.client = None if not self.client: # Fallback to standalone print("๐Ÿš€ Starting standalone MCP browser...") self.browser = MCPBrowser(server_name=self.server_name) await self.browser.initialize() print("โœ… MCP browser initialized") # Load initial tool list await self._refresh_tools() async def _refresh_tools(self): """Refresh the tool cache.""" try: response = await self._call_mcp({ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }) if "result" in response and "tools" in response["result"]: self.tool_cache.clear() for tool in response["result"]["tools"]: self.tool_cache[tool["name"]] = tool except Exception as e: self.logger.warning(f"Failed to refresh tools: {e}") async def _call_mcp(self, request: Dict[str, Any]) -> Dict[str, Any]: """Call MCP method through client or browser.""" if self.client: return await self.client.call(request) elif self.browser: return await self.browser.call(request) else: raise RuntimeError("No MCP connection available") async def run(self): """Main interactive loop.""" try: await self.initialize() while True: try: # Get user input line = input("mcp> ").strip() if not line: continue self.command_history.append(line) # Parse and execute command await self._execute_command(line) except KeyboardInterrupt: print("\nUse 'quit' to exit") continue except EOFError: break except Exception as e: print(f"โŒ Error: {e}") traceback.print_exc() finally: await self.cleanup() async def _execute_command(self, line: str): """Execute a user command.""" parts = line.split() if not parts: return command = parts[0].lower() args = parts[1:] if command == 'help': self._show_help() elif command == 'quit' or command == 'exit': print("๐Ÿ‘‹ Goodbye!") sys.exit(0) elif command == 'list': await self._list_tools(args) elif command == 'discover': await self._discover_tools(args) elif command == 'call': await self._call_tool(args) elif command == 'onboard': await self._manage_onboarding(args) elif command == 'status': await self._show_status() elif command == 'test': await self._test_tool(args) elif command == 'refresh': await self._refresh_tools() print("๐Ÿ”„ Tool cache refreshed") else: # Try to call it as a tool directly await self._call_tool_direct(command, args) def _show_help(self): """Show help information.""" help_text = """ ๐Ÿ” MCP Browser Interactive Commands Basic Commands: help Show this help quit, exit Exit the browser refresh Refresh tool cache status Show connection status Tool Discovery: list [pattern] List available tools (optional filter) discover Discover tools using JSONPath Tool Execution: call [args...] Call a tool with arguments test Test a tool with sample data [args...] Direct tool call (shortcut) Onboarding: onboard Get onboarding for identity onboard Set onboarding Examples: list # List all tools list bash # List tools containing 'bash' discover $.tools[*].name # Get all tool names discover $.tools[?(@.name=='Bash')] # Get Bash tool details call mcp_discover jsonpath="$.tools[*].name" test Bash # Test Bash tool onboard Claude # Get Claude's onboarding onboard Claude "Focus on code quality" # Set onboarding """ print(help_text) async def _list_tools(self, args: List[str]): """List available tools with optional filtering.""" pattern = args[0] if args else None tools = list(self.tool_cache.values()) if pattern: tools = [t for t in tools if pattern.lower() in t["name"].lower() or pattern.lower() in t.get("description", "").lower()] if not tools: print("โŒ No tools found") return print(f"๐Ÿ“‹ Available Tools ({len(tools)} found):") print() for tool in tools: name = tool["name"] desc = tool.get("description", "No description") # Truncate long descriptions if len(desc) > 80: desc = desc[:77] + "..." # Add emoji based on tool type emoji = "๐Ÿ”" if "discover" in name else "๐Ÿš€" if "call" in name else "๐Ÿ“‹" if "onboard" in name else "๐Ÿ› ๏ธ" print(f" {emoji} {name}") print(f" {desc}") print() async def _discover_tools(self, args: List[str]): """Discover tools using JSONPath.""" if not args: print("โŒ Usage: discover ") print("Examples:") print(" discover $.tools[*].name") print(" discover $.tools[?(@.name=='Bash')]") return jsonpath = " ".join(args) try: response = await self._call_mcp({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "mcp_discover", "arguments": {"jsonpath": jsonpath} } }) if "result" in response: content = response["result"].get("content", []) if content and content[0].get("type") == "text": result = content[0]["text"] print("๐Ÿ” Discovery Result:") print(result) else: print("โŒ No content in response") elif "error" in response: print(f"โŒ Error: {response['error']['message']}") except Exception as e: print(f"โŒ Discovery failed: {e}") async def _call_tool(self, args: List[str]): """Call a tool with arguments.""" if not args: print("โŒ Usage: call [key=value...]") print("Example: call mcp_discover jsonpath=\"$.tools[*].name\"") return tool_name = args[0] # Parse key=value arguments arguments = {} for arg in args[1:]: if "=" in arg: key, value = arg.split("=", 1) # Remove quotes if present value = value.strip('"\'') arguments[key] = value else: # Positional argument - try to guess the parameter name if tool_name in self.tool_cache: tool = self.tool_cache[tool_name] schema = tool.get("inputSchema", {}) props = schema.get("properties", {}) required = schema.get("required", []) # Use first required parameter if required and len(arguments) == 0: arguments[required[0]] = arg else: arguments[f"arg_{len(arguments)}"] = arg await self._execute_tool_call(tool_name, arguments) async def _call_tool_direct(self, tool_name: str, args: List[str]): """Direct tool call (shortcut syntax).""" if tool_name not in self.tool_cache: print(f"โŒ Unknown tool: {tool_name}") print("Use 'list' to see available tools") return # Parse arguments like _call_tool arguments = {} for arg in args: if "=" in arg: key, value = arg.split("=", 1) value = value.strip('"\'') arguments[key] = value else: # Use tool schema to guess parameter tool = self.tool_cache[tool_name] schema = tool.get("inputSchema", {}) required = schema.get("required", []) if required and len(arguments) == 0: arguments[required[0]] = arg await self._execute_tool_call(tool_name, arguments) async def _execute_tool_call(self, tool_name: str, arguments: Dict[str, Any]): """Execute a tool call and display results.""" print(f"๐Ÿš€ Calling {tool_name} with {arguments}") try: response = await self._call_mcp({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": tool_name, "arguments": arguments } }) if "result" in response: self._display_result(response["result"]) elif "error" in response: print(f"โŒ Error: {response['error']['message']}") except Exception as e: print(f"โŒ Tool call failed: {e}") def _display_result(self, result: Any): """Display tool call result in a nice format.""" if isinstance(result, dict) and "content" in result: # MCP content format content = result["content"] for item in content: if item.get("type") == "text": print("๐Ÿ“„ Result:") print(item["text"]) elif item.get("type") == "image": print(f"๐Ÿ–ผ๏ธ Image: {item.get('url', 'No URL')}") else: print(f"๐Ÿ“ฆ Content: {json.dumps(item, indent=2)}") else: # Raw result print("๐Ÿ“ฆ Result:") if isinstance(result, (dict, list)): print(json.dumps(result, indent=2)) else: print(str(result)) async def _test_tool(self, args: List[str]): """Test a tool with sample data.""" if not args: print("โŒ Usage: test ") return tool_name = args[0] if tool_name not in self.tool_cache: print(f"โŒ Unknown tool: {tool_name}") return tool = self.tool_cache[tool_name] schema = tool.get("inputSchema", {}) print(f"๐Ÿงช Testing {tool_name}") print(f"๐Ÿ“‹ Description: {tool.get('description', 'No description')}") print(f"๐Ÿ“Š Schema: {json.dumps(schema, indent=2)}") # Generate sample arguments sample_args = self._generate_sample_args(schema) print(f"๐ŸŽฒ Sample arguments: {sample_args}") # Ask user if they want to proceed try: confirm = input("Proceed with test? [y/N]: ").strip().lower() if confirm in ['y', 'yes']: await self._execute_tool_call(tool_name, sample_args) except KeyboardInterrupt: print("\nโŒ Test cancelled") def _generate_sample_args(self, schema: Dict[str, Any]) -> Dict[str, Any]: """Generate sample arguments based on schema.""" args = {} props = schema.get("properties", {}) for name, prop in props.items(): prop_type = prop.get("type", "string") if prop_type == "string": if "example" in prop: args[name] = prop["example"] elif name.lower() in ["jsonpath", "path"]: args[name] = "$.tools[*].name" elif name.lower() in ["query", "search"]: args[name] = "test query" else: args[name] = f"sample_{name}" elif prop_type == "boolean": args[name] = False elif prop_type == "number": args[name] = 1 elif prop_type == "array": args[name] = ["sample"] elif prop_type == "object": args[name] = {} return args async def _manage_onboarding(self, args: List[str]): """Manage onboarding instructions.""" if not args: print("โŒ Usage: onboard [instructions]") return identity = args[0] instructions = " ".join(args[1:]) if len(args) > 1 else None arguments = {"identity": identity} if instructions: arguments["instructions"] = instructions await self._execute_tool_call("onboarding", arguments) async def _show_status(self): """Show connection and tool status.""" print("๐Ÿ“Š MCP Browser Status") print() if self.client: print("๐Ÿ”— Connection: Daemon") elif self.browser: print("๐Ÿ”— Connection: Standalone") else: print("โŒ Connection: None") print(f"๐Ÿ› ๏ธ Tools cached: {len(self.tool_cache)}") print(f"๐Ÿ“ Command history: {len(self.command_history)}") if self.server_name: print(f"๐ŸŽฏ Server: {self.server_name}") # Show tool breakdown if self.tool_cache: meta_tools = [name for name in self.tool_cache if name.startswith("mcp_") or name == "onboarding"] regular_tools = [name for name in self.tool_cache if name not in meta_tools] print() print(f"๐Ÿ” Meta tools: {len(meta_tools)} ({', '.join(meta_tools)})") print(f"๐Ÿ› ๏ธ Regular tools: {len(regular_tools)}") async def cleanup(self): """Cleanup resources.""" try: if self.client: await self.client.__aexit__(None, None, None) if self.browser: await self.browser.close() except Exception as e: self.logger.warning(f"Cleanup error: {e}") async def main(): """Main entry point for interactive mode.""" import argparse parser = argparse.ArgumentParser(description="Interactive MCP Browser") parser.add_argument("--server", help="MCP server name") parser.add_argument("--no-daemon", action="store_true", help="Don't use daemon") args = parser.parse_args() client = InteractiveMCPClient( server_name=args.server, use_daemon=not args.no_daemon ) await client.run() if __name__ == "__main__": asyncio.run(main())