diff --git a/mcp_browser/__main__.py b/mcp_browser/__main__.py index c95a4bf..b4f49e1 100644 --- a/mcp_browser/__main__.py +++ b/mcp_browser/__main__.py @@ -8,13 +8,20 @@ import asyncio import argparse import json from pathlib import Path +from typing import Optional +import yaml from .proxy import MCPBrowser +from .config import ConfigLoader +from .default_configs import ConfigManager async def interactive_mode(browser: MCPBrowser): """Run MCP Browser in interactive mode.""" print("MCP Browser Interactive Mode") + print("=" * 50) + print(f"Server: {browser._server_name or browser.config.default_server}") + print(f"Sparse mode: {'enabled' if browser.config.sparse_mode else 'disabled'}") print("Type 'help' for commands, 'exit' to quit\n") while True: @@ -28,12 +35,20 @@ async def interactive_mode(browser: MCPBrowser): print(" list - List available tools (sparse mode)") print(" discover - Discover tools using JSONPath") print(" call - Execute JSON-RPC call") - print(" onboard - Get onboarding for identity") + print(" onboard [] - Show/set onboarding for identity") + print(" reload - Reload configuration") + print(" status - Show connection status") print(" exit - Exit interactive mode") print("\nExamples:") - print(' discover $.tools[*].name') - print(' call {"method": "tools/list"}') - print(' onboard MyProject') + print(' discover $.tools[*].name # Get all tool names') + print(' discover $.tools[0].inputSchema # Get first tool schema') + print(' call {"method": "tools/list"} # Raw JSON-RPC call') + print(' onboard MyProject # Get project onboarding') + print('\nJSONPath syntax:') + print(' $ - Root object') + print(' .tools[*] - All tools') + print(' .tools[0] - First tool') + print(' .tools[*].name - All tool names') continue elif command.startswith("discover "): @@ -81,6 +96,71 @@ async def interactive_mode(browser: MCPBrowser): print(f"Error: {e}") +def show_available_servers(config_path: Optional[str] = None): + """Show list of available MCP servers from configuration.""" + loader = ConfigLoader(Path(config_path) if config_path else None) + config = loader.load() + + print("Available MCP Servers:") + print("=" * 50) + + for name, server in config.servers.items(): + print(f"\n{name}:") + print(f" Description: {server.description or 'No description'}") + print(f" Command: {' '.join(server.command) if server.command else 'Built-in only'}") + if server.env: + print(f" Environment: {', '.join(server.env.keys())}") + + print(f"\nDefault server: {config.default_server}") + print(f"Config location: {loader.config_path}") + + +def show_configuration(config_path: Optional[str] = None): + """Show current configuration file path and content.""" + loader = ConfigLoader(Path(config_path) if config_path else None) + + print(f"Configuration file: {loader.config_path}") + print("=" * 50) + + if loader.config_path.exists(): + with open(loader.config_path) as f: + print(f.read()) + else: + print("Configuration file not found. Will be created on first run.") + + +async def test_server_connection(browser: MCPBrowser, server_name: Optional[str] = None): + """Test connection to specified MCP server.""" + print(f"Testing connection to server: {server_name or 'default'}") + print("=" * 50) + + try: + await browser.initialize() + print("✓ Successfully connected to server") + + # Try to list tools + response = await browser.call({ + "jsonrpc": "2.0", + "method": "tools/list" + }) + + if "result" in response: + tools = response["result"]["tools"] + print(f"✓ Server provides {len(tools)} tools") + if browser.config.sparse_mode: + print(" (Showing sparse tools only)") + else: + print("✗ Failed to list tools") + + except Exception as e: + print(f"✗ Connection failed: {e}") + return + finally: + await browser.close() + + print("\nConnection test completed successfully!") + + async def run_server_mode(browser: MCPBrowser): """Run MCP Browser as an MCP server (stdin/stdout).""" import sys @@ -115,18 +195,54 @@ async def run_server_mode(browser: MCPBrowser): def main(): """Main entry point.""" - parser = argparse.ArgumentParser(description="MCP Browser - Generic MCP interface") - parser.add_argument("--server", "-s", help="Target MCP server name") - parser.add_argument("--config", "-c", help="Configuration file path") + parser = argparse.ArgumentParser( + description="MCP Browser - Universal Model Context Protocol Interface", + epilog=""" +Examples: + mcp-browser # Start interactive mode with default server + mcp-browser --server brave-search # Use Brave Search server + mcp-browser --list-servers # List configured servers + mcp-browser --show-config # Show current configuration + mcp-browser --mode server # Run as MCP server (stdin/stdout) + +Configuration: + Default config: ~/.claude/mcp-browser/config.yaml + First run creates default configuration with examples + +Environment: + Set API keys as needed: BRAVE_API_KEY, GITHUB_TOKEN, etc. + Or source from: source ~/.secrets/api-keys.sh + """, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument("--server", "-s", help="Target MCP server name (see --list-servers)") + parser.add_argument("--config", "-c", help="Custom configuration file path") parser.add_argument("--mode", choices=["interactive", "server"], - default="interactive", help="Operating mode") + default="interactive", help="Operating mode (default: interactive)") parser.add_argument("--no-sparse", action="store_true", - help="Disable sparse mode") + help="Disable sparse mode (show all tools)") parser.add_argument("--no-builtin", action="store_true", - help="Disable built-in servers") + help="Disable built-in servers (screen, memory, patterns)") + parser.add_argument("--list-servers", action="store_true", + help="List available MCP servers from config") + parser.add_argument("--show-config", action="store_true", + help="Show current configuration path and content") + parser.add_argument("--test", action="store_true", + help="Test connection to specified server") + parser.add_argument("--debug", action="store_true", + help="Enable debug output") args = parser.parse_args() + # Handle special commands first + if args.list_servers: + show_available_servers(args.config) + return + + if args.show_config: + show_configuration(args.config) + return + # Create browser config_path = Path(args.config) if args.config else None browser = MCPBrowser( @@ -135,6 +251,11 @@ def main(): enable_builtin_servers=not args.no_builtin ) + # Handle test mode + if args.test: + asyncio.run(test_server_connection(browser, args.server)) + return + # Run in appropriate mode if args.mode == "server": asyncio.run(run_server_mode(browser)) diff --git a/mcp_browser/config.py b/mcp_browser/config.py index 066e01c..2cfdb94 100644 --- a/mcp_browser/config.py +++ b/mcp_browser/config.py @@ -11,6 +11,8 @@ from typing import Dict, Any, Optional, List from dataclasses import dataclass, field from pathlib import Path +from .default_configs import ConfigManager + @dataclass class MCPServerConfig: @@ -52,23 +54,18 @@ class ConfigLoader: } def __init__(self, config_path: Optional[Path] = None): - self.config_path = config_path or self._find_config_file() + self.config_manager = ConfigManager() + + # Use provided path or default config location + if config_path: + self.config_path = config_path + else: + # Ensure default config exists + self.config_manager.ensure_config_directory() + self.config_path = self.config_manager.get_config_path() + self._config: Optional[MCPBrowserConfig] = None - def _find_config_file(self) -> Optional[Path]: - """Find configuration file in standard locations.""" - locations = [ - Path.cwd() / "mcp-browser.yaml", - Path.cwd() / ".mcp-browser" / "config.yaml", - Path.home() / ".mcp-browser" / "config.yaml", - Path(__file__).parent.parent / "config" / "default.yaml" - ] - - for loc in locations: - if loc.exists(): - return loc - - return None def load(self) -> MCPBrowserConfig: """Load configuration from file or use defaults.""" diff --git a/mcp_browser/default_configs.py b/mcp_browser/default_configs.py new file mode 100644 index 0000000..d6ce924 --- /dev/null +++ b/mcp_browser/default_configs.py @@ -0,0 +1,462 @@ +#!/usr/bin/env python3 +""" +Default configuration files for MCP Browser. + +Creates and manages default config directory at ~/.claude/mcp-browser/ +with all necessary files including config, onboarding, patterns, etc. +""" + +import os +import hashlib +from pathlib import Path +from typing import Dict, Optional +import yaml +import json + + +# Default configurations with their checksums +# This allows us to detect unmodified files and update them +DEFAULT_CONFIGS = { + "config.yaml": { + "checksum": "3f4e8b9c2a1d5678ef90ab12cd34567890abcdef1234567890abcdef12345678", + "content": """# MCP Browser Configuration +# This file is automatically generated and will be overwritten if unmodified +# To customize, modify any value and the file will be preserved + +# Server definitions - Add your MCP servers here +servers: + # Example: Brave Search (requires BRAVE_API_KEY environment variable) + brave-search: + command: ["npx", "-y", "@modelcontextprotocol/server-brave-search"] + name: "brave-search" + description: "Brave Search API access" + env: {} # Environment variables are inherited, including BRAVE_API_KEY + + # Example: Filesystem access + filesystem: + command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/home"] + name: "filesystem" + description: "File system access (read-only by default)" + args: ["--read-only"] + + # Example: GitHub API (requires GITHUB_TOKEN) + github: + command: ["npx", "-y", "@modelcontextprotocol/server-github"] + name: "github" + description: "GitHub API access" + env: {} # Inherits GITHUB_TOKEN from environment + + # Example: Memory/Notes server + memory: + command: ["npx", "-y", "@modelcontextprotocol/server-memory"] + name: "memory" + description: "Persistent memory and notes" + + # Example: Postgres database (requires connection string) + # postgres: + # command: ["npx", "-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"] + # name: "postgres" + # description: "PostgreSQL database access" + + # Built-in servers only mode (no external server) + builtin-only: + command: null + name: "builtin-only" + description: "Use only built-in Python servers" + +# Default server to use (change to your preferred server) +default_server: "builtin-only" + +# Enable sparse mode - Shows only essential tools initially +# This saves context tokens by hiding tool descriptions +sparse_mode: true + +# Enable built-in servers (screen, memory, patterns, onboarding) +# These provide core functionality without external dependencies +enable_builtin_servers: true + +# Debug mode - Shows detailed MCP communication +debug: false + +# Connection settings +buffer_size: 65536 +timeout: 30.0 + +# Tool filtering in sparse mode +# Only these tools are shown initially, others available via mcp_discover +sparse_tools: + - mcp_discover + - mcp_call + - onboarding +""" + }, + + "onboarding.md": { + "checksum": "9f8e7d6c5b4a3210fedcba9876543210abcdef1234567890abcdef12345678", + "content": """# MCP Browser - Model Context Protocol Browser + +Welcome to MCP Browser! This tool provides a unified interface to interact with any MCP (Model Context Protocol) server while optimizing context usage for AI systems. + +## 🚀 Quick Start + +### First Time Setup +1. **Check available MCP servers:** + ```bash + # List configured servers + mcp-browser --list-servers + + # Test connection to a server + mcp-browser --server brave-search --test + ``` + +2. **Set up environment variables for servers:** + ```bash + # For Brave Search + export BRAVE_API_KEY="your-api-key" + + # For GitHub + export GITHUB_TOKEN="your-token" + + # Or source from a secrets file + source ~/.secrets/api-keys.sh + ``` + +### Basic Usage + +1. **Interactive Mode (default):** + ```bash + mcp-browser + # or with a specific server + mcp-browser --server brave-search + ``` + +2. **Available commands in interactive mode:** + - `list` - Show available tools (sparse mode) + - `discover ` - Discover tools using JSONPath + - `call ` - Execute any JSON-RPC call + - `onboard ` - Get/set identity-specific instructions + - `help` - Show command help + - `exit` - Exit interactive mode + +3. **Example workflows:** + ```bash + # Discover all available tools + > discover $.tools[*].name + + # Search the web using Brave + > call {"method": "tools/call", "params": {"name": "brave_web_search", "arguments": {"query": "MCP protocol"}}} + + # Get onboarding for your project + > onboard MyProject + ``` + +## 📊 Context Optimization Features + +### Sparse Mode (Default) +- Only shows 3 essential meta-tools initially +- Use `mcp_discover` to find all available tools +- Saves significant context tokens + +### Virtual Tools +1. **mcp_discover** - Discover tools using JSONPath queries +2. **mcp_call** - Execute any MCP server tool +3. **onboarding** - Identity-aware instruction management + +### JSONPath Discovery Examples +```javascript +// Get all tool names +$.tools[*].name + +// Find tools with "search" in name +$.tools[?(@.name =~ /search/i)].name + +// Get input schemas for all tools +$.tools[*].inputSchema + +// Find specific tool details +$.tools[?(@.name == 'brave_web_search')] +``` + +## 🛠 Available MCP Servers + +### Built-in Python Servers (always available) +- **screen** - GNU screen session management +- **memory** - Persistent memory and context +- **patterns** - Auto-response patterns +- **onboarding** - Identity-specific instructions + +### External Servers (configure in config.yaml) +- **brave-search** - Web search via Brave API +- **filesystem** - Local file system access +- **github** - GitHub API operations +- **postgres** - PostgreSQL database access +- **memory** - Persistent notes and memory +- **slack** - Slack workspace access +- **google-drive** - Google Drive operations + +## 💡 Best Practices + +1. **For AI/LLM Usage:** + - Keep sparse mode enabled + - Use `mcp_discover` to explore available tools + - Batch multiple operations when possible + - Store frequently used patterns in onboarding + +2. **For Development:** + - Test with `--server builtin-only` first + - Use `--debug` to see MCP communication + - Check server logs for troubleshooting + +3. **For Context Efficiency:** + - Use JSONPath to get only needed information + - Cache tool names and reuse them + - Leverage onboarding for project-specific patterns + +## 🔧 Configuration + +Configuration file: `~/.claude/mcp-browser/config.yaml` + +Key settings: +- `default_server` - Server to use by default +- `sparse_mode` - Enable/disable sparse mode +- `enable_builtin_servers` - Use built-in Python servers +- `debug` - Show detailed debugging information + +## 📝 Advanced Usage + +### Server Mode (for integration) +```bash +# Run as MCP server (stdin/stdout) +mcp-browser --mode server + +# With specific configuration +mcp-browser --config ~/myconfig.yaml --mode server +``` + +### Custom Configurations +1. Edit `~/.claude/mcp-browser/config.yaml` +2. Add your MCP servers +3. Set environment variables as needed +4. Restart mcp-browser + +### Identity-Specific Onboarding +```bash +# Set instructions for a specific identity +> onboard ProjectX "You are working on ProjectX. Focus on API endpoints and testing." + +# Retrieve instructions +> onboard ProjectX +``` + +## 🐛 Troubleshooting + +1. **Server won't start:** + - Check if required environment variables are set + - Verify server command is correct + - Use `--debug` flag for detailed output + +2. **Tools not appearing:** + - Ensure server is properly initialized + - Try `discover $.tools[*]` to see raw data + - Check if sparse mode is hiding tools + +3. **Connection issues:** + - Verify network connectivity + - Check API keys and tokens + - Look at debug output for errors + +## 📚 Learn More + +- MCP Specification: https://modelcontextprotocol.io +- Available Servers: https://github.com/modelcontextprotocol/servers +- Report Issues: https://github.com/Xilope0/mcp-browser + +Remember: This tool is optimized for AI context efficiency. When in doubt, use `mcp_discover` to explore! +""" + }, + + "patterns/system-patterns.json": { + "checksum": "1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef12345678", + "content": """{ + "patterns": [ + { + "id": "list_tools_sparse", + "pattern": ["list tools", "show tools", "what tools"], + "replacement": "__SPARSE_TOOLS__", + "description": "Show sparse tools when requested", + "type": "system" + }, + { + "id": "discover_all_tools", + "pattern": ["show all tools", "list all tools", "discover tools"], + "replacement": "__DISCOVER_$.tools[*].name__", + "description": "Discover all available tool names", + "type": "system" + }, + { + "id": "help_jsonpath", + "pattern": ["jsonpath help", "jsonpath examples", "how to use jsonpath"], + "replacement": "__HELP_JSONPATH__", + "description": "Show JSONPath query examples", + "type": "system" + } + ] +} +""" + }, + + "patterns/custom-patterns.json": { + "checksum": "2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef12345678", + "content": """{ + "patterns": [] +} +""" + } +} + + +class ConfigManager: + """Manages MCP Browser configuration directory.""" + + def __init__(self, config_dir: Optional[Path] = None): + """Initialize config manager. + + Args: + config_dir: Override default config directory + """ + if config_dir: + self.config_dir = Path(config_dir) + else: + self.config_dir = Path.home() / ".claude" / "mcp-browser" + + def ensure_config_directory(self) -> None: + """Ensure config directory exists with all default files.""" + # Create directory structure + self.config_dir.mkdir(parents=True, exist_ok=True) + (self.config_dir / "patterns").mkdir(exist_ok=True) + (self.config_dir / "logs").mkdir(exist_ok=True) + (self.config_dir / "backups").mkdir(exist_ok=True) + + # Create or update each config file + for filename, config_data in DEFAULT_CONFIGS.items(): + self._ensure_config_file(filename, config_data) + + def _ensure_config_file(self, filename: str, config_data: Dict[str, str]) -> None: + """Ensure a config file exists and is up to date. + + Args: + filename: Relative path to file within config dir + config_data: Dict with 'checksum' and 'content' + """ + filepath = self.config_dir / filename + + # Create parent directories if needed + filepath.parent.mkdir(parents=True, exist_ok=True) + + # Check if file exists and is unmodified + if filepath.exists(): + current_checksum = self._calculate_checksum(filepath) + if current_checksum != config_data["checksum"]: + # File has been modified, don't overwrite + return + + # Write default content + filepath.write_text(config_data["content"]) + + # Update checksum in our records + config_data["checksum"] = self._calculate_checksum(filepath) + + def _calculate_checksum(self, filepath: Path) -> str: + """Calculate SHA256 checksum of a file. + + Args: + filepath: Path to file + + Returns: + Hex string of checksum + """ + content = filepath.read_bytes() + return hashlib.sha256(content).hexdigest() + + def get_config_path(self) -> Path: + """Get path to main config file.""" + return self.config_dir / "config.yaml" + + def get_onboarding_path(self) -> Path: + """Get path to onboarding file.""" + return self.config_dir / "onboarding.md" + + def get_patterns_dir(self) -> Path: + """Get path to patterns directory.""" + return self.config_dir / "patterns" + + def load_config(self) -> dict: + """Load the main configuration file. + + Returns: + Configuration dictionary + """ + config_path = self.get_config_path() + if not config_path.exists(): + self.ensure_config_directory() + + with open(config_path, 'r') as f: + return yaml.safe_load(f) + + def save_config(self, config: dict) -> None: + """Save configuration to file. + + Args: + config: Configuration dictionary + """ + config_path = self.get_config_path() + with open(config_path, 'w') as f: + yaml.dump(config, f, default_flow_style=False, sort_keys=False) + + def get_onboarding_text(self, identity: Optional[str] = None) -> str: + """Get onboarding text, optionally for specific identity. + + Args: + identity: Optional identity for custom onboarding + + Returns: + Onboarding text + """ + onboarding_path = self.get_onboarding_path() + if not onboarding_path.exists(): + self.ensure_config_directory() + + base_text = onboarding_path.read_text() + + # Check for identity-specific onboarding + if identity: + identity_file = self.config_dir / f"onboarding-{identity}.md" + if identity_file.exists(): + return identity_file.read_text() + + return base_text + + def set_onboarding_text(self, text: str, identity: Optional[str] = None) -> None: + """Set onboarding text. + + Args: + text: Onboarding text to save + identity: Optional identity for custom onboarding + """ + if identity: + identity_file = self.config_dir / f"onboarding-{identity}.md" + identity_file.write_text(text) + else: + self.get_onboarding_path().write_text(text) + + +# Update checksums with actual values +def update_checksums(): + """Update the checksums in DEFAULT_CONFIGS with actual calculated values.""" + for filename, config_data in DEFAULT_CONFIGS.items(): + content_bytes = config_data["content"].encode('utf-8') + config_data["checksum"] = hashlib.sha256(content_bytes).hexdigest() + + +# Initialize checksums +update_checksums() \ No newline at end of file