Skip to main content
Glama
8b-is
by 8b-is
mcp_explorer.py25.1 kB
#!/usr/bin/env python3 """ MCP Explorer - Interactive MCP Tool Explorer for Humans ======================================================== A universal MCP client that lets you explore and interact with any MCP server. Perfect for understanding what AI tools do and how they work! Author: Aye & Hue 🚢 """ import json import subprocess import sys import os from typing import Dict, List, Any, Optional from dataclasses import dataclass import argparse from pathlib import Path import textwrap import re # Try to import rich for beautiful terminal output try: from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.syntax import Syntax from rich.prompt import Prompt, Confirm from rich.markdown import Markdown from rich import print as rprint RICH_AVAILABLE = True except ImportError: RICH_AVAILABLE = False print("💡 Tip: Install 'rich' for a better experience: pip install rich") @dataclass class MCPTool: """Represents an MCP tool with its metadata""" name: str description: str parameters: Dict[str, Any] lane: Optional[str] = None # EXPLORE, ANALYZE, or ACT def get_emoji(self) -> str: """Get emoji based on tool lane""" if not self.lane: return "🔧" lane_emojis = { "EXPLORE": "🔍", "ANALYZE": "🧪", "ACT": "⚡" } return lane_emojis.get(self.lane, "🔧") class MCPExplorer: """Interactive MCP Explorer - Learn by doing!""" def __init__(self, server_command: List[str], verbose: bool = False): self.server_command = server_command self.verbose = verbose self.console = Console() if RICH_AVAILABLE else None self.tools: Dict[str, MCPTool] = {} self.server_info: Dict[str, Any] = {} self.history: List[Dict] = [] def print(self, *args, **kwargs): """Print with rich if available, otherwise standard print""" if self.console: self.console.print(*args, **kwargs) else: print(*args, **kwargs) def send_request(self, method: str, params: Optional[Dict] = None) -> Dict: """Send a JSON-RPC request to the MCP server""" request = { "jsonrpc": "2.0", "id": len(self.history) + 1, "method": method } if params: request["params"] = params if self.verbose: self.print(f"[dim]→ Sending: {json.dumps(request, indent=2)}[/dim]") try: # Run the MCP server with the request process = subprocess.Popen( self.server_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) # Send request and get response stdout, stderr = process.communicate(input=json.dumps(request)) if self.verbose and stderr: self.print(f"[dim yellow]Server logs: {stderr}[/dim yellow]") # Parse response - handle shell escaping stdout = stdout.strip() if stdout.startswith("'") and stdout.endswith("'"): stdout = stdout[1:-1] response = json.loads(stdout) if self.verbose: self.print(f"[dim]← Received: {json.dumps(response, indent=2)}[/dim]") self.history.append({"request": request, "response": response}) return response except json.JSONDecodeError as e: self.print(f"[red]Error parsing response: {e}[/red]") if stdout: self.print(f"Raw output: {stdout}") return {} except Exception as e: self.print(f"[red]Error communicating with server: {e}[/red]") return {} def initialize(self) -> bool: """Initialize connection to MCP server""" self.print("\n🚀 Connecting to MCP server...") # Try to get server info response = self.send_request("initialize", { "protocolVersion": "0.1.0", "capabilities": { "tools": {} }, "clientInfo": { "name": "MCP Explorer", "version": "1.0.0" } }) if "result" in response: self.server_info = response["result"].get("serverInfo", {}) self.print(f"✅ Connected to: [bold]{self.server_info.get('name', 'Unknown')}[/bold] v{self.server_info.get('version', '?')}") # Get available tools self.discover_tools() return True else: self.print("[red]Failed to initialize MCP connection[/red]") return False def discover_tools(self): """Discover available tools from the server""" response = self.send_request("tools/list") if "result" in response: tools_data = response["result"].get("tools", []) for tool_data in tools_data: # Extract lane from description if present desc = tool_data.get("description", "") lane = None if "EXPLORE:" in desc: lane = "EXPLORE" elif "ANALYZE:" in desc: lane = "ANALYZE" elif "ACT:" in desc: lane = "ACT" tool = MCPTool( name=tool_data["name"], description=desc, parameters=tool_data.get("inputSchema", {}), lane=lane ) self.tools[tool.name] = tool self.print(f"📦 Discovered [green]{len(self.tools)}[/green] tools") def display_tools(self, filter_lane: Optional[str] = None): """Display available tools in a nice format""" if RICH_AVAILABLE: table = Table(title="Available MCP Tools", show_lines=True) table.add_column("Tool", style="cyan", no_wrap=True) table.add_column("Lane", style="magenta") table.add_column("Description", style="white") for tool in self.tools.values(): if filter_lane and tool.lane != filter_lane: continue # Clean up description desc = re.sub(r'^(🔍 EXPLORE:|🧪 ANALYZE:|⚡ ACT:)\s*', '', tool.description) desc = textwrap.fill(desc, width=60) table.add_row( f"{tool.get_emoji()} {tool.name}", tool.lane or "General", desc ) self.console.print(table) else: # Simple text output print("\n=== Available MCP Tools ===\n") for tool in self.tools.values(): if filter_lane and tool.lane != filter_lane: continue print(f"{tool.get_emoji()} {tool.name}") print(f" Lane: {tool.lane or 'General'}") print(f" {textwrap.fill(tool.description, width=70, subsequent_indent=' ')}") print() def get_tool_params(self, tool: MCPTool) -> Dict[str, Any]: """Interactively get parameters for a tool""" params = {} schema = tool.parameters.get("properties", {}) required = tool.parameters.get("required", []) self.print(f"\n📝 Parameters for [cyan]{tool.name}[/cyan]:") for param_name, param_schema in schema.items(): param_type = param_schema.get("type", "string") description = param_schema.get("description", "") default = param_schema.get("default") is_required = param_name in required # Build prompt prompt = f" {param_name}" if description: prompt += f" ({description})" if not is_required: prompt += " [optional]" if default is not None: prompt += f" [default: {default}]" prompt += ": " # Get value based on type if param_type == "boolean": if RICH_AVAILABLE: value = Confirm.ask(prompt, default=default if default is not None else False) else: response = input(prompt + "(y/n) ").lower() value = response in ['y', 'yes', 'true', '1'] elif param_type == "integer": value_str = input(prompt) if value_str: try: value = int(value_str) except ValueError: self.print(f"[yellow]Invalid integer, using default[/yellow]") value = default else: value = default elif param_type == "array": value_str = input(prompt + "(comma-separated) ") if value_str: value = [v.strip() for v in value_str.split(",")] else: value = default else: # string or other value = input(prompt) if not value and default is not None: value = default # Only add if we have a value or it's required if value or is_required: params[param_name] = value return params def call_tool(self, tool_name: str, params: Optional[Dict] = None): """Call a specific tool and display results""" if tool_name not in self.tools: self.print(f"[red]Tool '{tool_name}' not found[/red]") return tool = self.tools[tool_name] # Get parameters interactively if not provided if params is None: params = self.get_tool_params(tool) self.print(f"\n🔄 Calling [cyan]{tool_name}[/cyan]...") response = self.send_request("tools/call", { "name": tool_name, "arguments": params }) if "result" in response: self.display_result(response["result"]) elif "error" in response: self.print(f"[red]Error: {response['error'].get('message', 'Unknown error')}[/red]") def display_result(self, result: Any): """Display tool result in a nice format""" if isinstance(result, dict) and "content" in result: # MCP standard response format for content_item in result["content"]: if content_item.get("type") == "text": text = content_item.get("text", "") # Try to parse as JSON for better display try: data = json.loads(text) if RICH_AVAILABLE: self.console.print(Panel( Syntax(json.dumps(data, indent=2), "json"), title="Result", border_style="green" )) else: print("\n=== Result ===") print(json.dumps(data, indent=2)) except json.JSONDecodeError: # Display as plain text if RICH_AVAILABLE: self.console.print(Panel(text, title="Result", border_style="green")) else: print("\n=== Result ===") print(text) else: # Raw result if RICH_AVAILABLE: self.console.print(Panel( Syntax(json.dumps(result, indent=2), "json"), title="Result", border_style="green" )) else: print("\n=== Result ===") print(json.dumps(result, indent=2)) def interactive_mode(self): """Run interactive exploration mode""" self.print("\n🎮 [bold]Interactive MCP Explorer[/bold]") self.print("Type 'help' for commands, 'quit' to exit\n") while True: try: if RICH_AVAILABLE: command = Prompt.ask("[bold blue]mcp[/bold blue]>").strip() else: command = input("mcp> ").strip() if not command: continue parts = command.split() cmd = parts[0].lower() if cmd in ['quit', 'exit', 'q']: self.print("👋 Goodbye!") break elif cmd == 'help': self.show_help() elif cmd == 'tools': filter_lane = parts[1].upper() if len(parts) > 1 else None self.display_tools(filter_lane) elif cmd == 'info': if len(parts) > 1: self.show_tool_info(parts[1]) else: self.show_server_info() elif cmd == 'call': if len(parts) > 1: self.call_tool(parts[1]) else: self.print("[yellow]Usage: call <tool_name>[/yellow]") elif cmd == 'lanes': self.show_lanes() elif cmd == 'history': self.show_history() elif cmd == 'guided': self.guided_exploration() else: self.print(f"[yellow]Unknown command: {cmd}[/yellow]") self.print("Type 'help' for available commands") except KeyboardInterrupt: self.print("\n[yellow]Use 'quit' to exit[/yellow]") except Exception as e: self.print(f"[red]Error: {e}[/red]") def show_help(self): """Display help information""" help_text = """ [bold]Available Commands:[/bold] [cyan]tools [lane][/cyan] - List all tools (optionally filtered by lane) [cyan]info <tool>[/cyan] - Show detailed info about a tool [cyan]call <tool>[/cyan] - Call a tool interactively [cyan]lanes[/cyan] - Show tool lanes (EXPLORE/ANALYZE/ACT) [cyan]guided[/cyan] - Start guided exploration [cyan]history[/cyan] - Show command history [cyan]help[/cyan] - Show this help [cyan]quit[/cyan] - Exit the explorer [bold]Tool Lanes:[/bold] 🔍 [green]EXPLORE[/green] - Discovery and overview tools 🧪 [green]ANALYZE[/green] - Deep analysis and search tools ⚡ [green]ACT[/green] - Tools that modify or create [bold]Examples:[/bold] tools explore - Show only EXPLORE tools info quick_tree - Get details about quick_tree call search_in_files - Search for content in files guided - Let me guide you through the tools! """ if RICH_AVAILABLE: self.console.print(Markdown(help_text)) else: print(help_text) def show_tool_info(self, tool_name: str): """Show detailed information about a specific tool""" if tool_name not in self.tools: self.print(f"[red]Tool '{tool_name}' not found[/red]") return tool = self.tools[tool_name] if RICH_AVAILABLE: panel_content = f""" [bold]Description:[/bold] {tool.description} [bold]Lane:[/bold] {tool.lane or 'General'} {tool.get_emoji()} [bold]Parameters:[/bold] """ schema = tool.parameters.get("properties", {}) required = tool.parameters.get("required", []) for param_name, param_schema in schema.items(): param_type = param_schema.get("type", "string") description = param_schema.get("description", "") is_required = param_name in required panel_content += f"\n • [cyan]{param_name}[/cyan] ({param_type})" if is_required: panel_content += " [red]*required[/red]" if description: panel_content += f"\n {description}" self.console.print(Panel( panel_content, title=f"Tool: {tool.name}", border_style="cyan" )) else: print(f"\n=== Tool: {tool.name} ===") print(f"Description: {tool.description}") print(f"Lane: {tool.lane or 'General'} {tool.get_emoji()}") print("\nParameters:") schema = tool.parameters.get("properties", {}) required = tool.parameters.get("required", []) for param_name, param_schema in schema.items(): param_type = param_schema.get("type", "string") description = param_schema.get("description", "") is_required = param_name in required print(f" • {param_name} ({param_type})", end="") if is_required: print(" *required", end="") print() if description: print(f" {description}") def show_lanes(self): """Show tools organized by lanes""" lanes = {"EXPLORE": [], "ANALYZE": [], "ACT": [], "General": []} for tool in self.tools.values(): lane = tool.lane or "General" lanes[lane].append(tool) if RICH_AVAILABLE: for lane_name, tools in lanes.items(): if not tools: continue emoji = {"EXPLORE": "🔍", "ANALYZE": "🧪", "ACT": "⚡", "General": "🔧"}[lane_name] self.console.print(f"\n[bold]{emoji} {lane_name} Lane[/bold] ({len(tools)} tools)") for tool in tools: self.console.print(f" • {tool.name}") else: for lane_name, tools in lanes.items(): if not tools: continue print(f"\n=== {lane_name} Lane ({len(tools)} tools) ===") for tool in tools: print(f" • {tool.name}") def show_server_info(self): """Display server information""" if RICH_AVAILABLE: info = f""" [bold]Server:[/bold] {self.server_info.get('name', 'Unknown')} [bold]Version:[/bold] {self.server_info.get('version', 'Unknown')} [bold]Total Tools:[/bold] {len(self.tools)} """ self.console.print(Panel(info, title="MCP Server Info", border_style="blue")) else: print("\n=== MCP Server Info ===") print(f"Server: {self.server_info.get('name', 'Unknown')}") print(f"Version: {self.server_info.get('version', 'Unknown')}") print(f"Total Tools: {len(self.tools)}") def show_history(self): """Display command history""" if not self.history: self.print("[yellow]No history yet[/yellow]") return for i, item in enumerate(self.history[-10:], 1): # Show last 10 req = item["request"] self.print(f"{i}. {req.get('method', 'unknown')} ", end="") if "params" in req and "name" in req["params"]: self.print(f"- {req['params']['name']}") else: self.print() def guided_exploration(self): """Guided exploration for beginners""" self.print("\n🎯 [bold]Guided Exploration Mode[/bold]") self.print("Let me guide you through the tools step by step!\n") # Step 1: Choose a lane self.print("[bold]Step 1: Choose your exploration path[/bold]") self.print("1. 🔍 EXPLORE - Start with overview and discovery") self.print("2. 🧪 ANALYZE - Deep dive into code and content") self.print("3. ⚡ ACT - Make changes and modifications") choice = input("\nYour choice (1-3): ").strip() if choice == "1": lane = "EXPLORE" self.print("\n[green]Great choice! Let's start exploring.[/green]") recommended_tool = "quick_tree" elif choice == "2": lane = "ANALYZE" self.print("\n[green]Perfect! Let's analyze some code.[/green]") recommended_tool = "search_in_files" elif choice == "3": lane = "ACT" self.print("\n[green]Powerful! Let's make some changes.[/green]") recommended_tool = "smart_edit" else: self.print("[yellow]Invalid choice, starting with EXPLORE[/yellow]") lane = "EXPLORE" recommended_tool = "quick_tree" # Step 2: Show relevant tools self.print(f"\n[bold]Step 2: Available {lane} tools:[/bold]") lane_tools = [t for t in self.tools.values() if t.lane == lane] for i, tool in enumerate(lane_tools[:5], 1): # Show first 5 self.print(f"{i}. {tool.get_emoji()} {tool.name}") desc = tool.description[:100] + "..." if len(tool.description) > 100 else tool.description self.print(f" {desc}") # Step 3: Recommend a tool if recommended_tool in self.tools: self.print(f"\n[bold]Step 3: I recommend starting with '{recommended_tool}'[/bold]") if input("Would you like to try it? (y/n): ").lower() == 'y': self.call_tool(recommended_tool) self.print("\n[green]Great job! You can now explore other tools with 'tools' or 'call <tool>'[/green]") def main(): parser = argparse.ArgumentParser( description="MCP Explorer - Interactive tool explorer for humans", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Explore Smart Tree MCP tools python mcp_explorer.py --command "st --mcp" # Explore any MCP server python mcp_explorer.py --command "your-mcp-server" # With verbose output python mcp_explorer.py --command "st --mcp" --verbose # Use a specific config file python mcp_explorer.py --config ~/.config/mcp/servers.json --server smart-tree """ ) parser.add_argument( "--command", type=str, help="Command to run the MCP server (e.g., 'st --mcp')" ) parser.add_argument( "--config", type=str, help="Path to MCP config file (for Claude Desktop compatibility)" ) parser.add_argument( "--server", type=str, help="Server name from config file" ) parser.add_argument( "--verbose", action="store_true", help="Show detailed request/response logs" ) args = parser.parse_args() # Determine server command if args.command: server_command = args.command.split() elif args.config and args.server: # Read from config file (Claude Desktop format) try: with open(args.config) as f: config = json.load(f) if args.server in config.get("mcpServers", {}): server_config = config["mcpServers"][args.server] server_command = server_config["command"].split() if "args" in server_config: server_command.extend(server_config["args"]) else: print(f"Server '{args.server}' not found in config") sys.exit(1) except Exception as e: print(f"Error reading config: {e}") sys.exit(1) else: # Default to Smart Tree if available server_command = ["st", "--mcp"] print("No command specified, trying default: st --mcp") print("Use --command to specify a different MCP server\n") # Create and run explorer explorer = MCPExplorer(server_command, verbose=args.verbose) if explorer.initialize(): explorer.interactive_mode() else: print("Failed to connect to MCP server") print("Make sure the server is installed and the command is correct") sys.exit(1) if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8b-is/smart-tree'

If you have feedback or need assistance with the MCP directory API, please join our Discord server