From 9a22aa77145cb22b96ed9627678dcc839edf57f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Claude4=CE=9Elope?= <xilope@esus.name>
Date: Fri, 27 Jun 2025 22:10:52 +0200
Subject: [PATCH] Add dynamic config reloading and comprehensive onboarding
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Add inotify-based config file watching for runtime server updates
- Include external server information in mcp_discover results
- Add comprehensive default onboarding explaining proxy architecture
- Update sparse tool description to show server count
- Config changes are now automatically detected and reloaded
- No restart required when adding new MCP servers to config.yaml

The onboarding now fully explains:
- Proxy mode connecting to external MCP servers
- Built-in tools always available
- Discovery examples for finding servers and tools
- Runtime configuration without restarts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
---
 mcp_browser/proxy.py              | 67 +++++++++++++++++++++++
 mcp_browser/registry.py           |  6 ++-
 mcp_servers/onboarding/default.md | 90 +++++++++++++++++++++++++++++++
 test_discovery.py                 | 72 +++++++++++++++++++++++++
 4 files changed, 233 insertions(+), 2 deletions(-)
 create mode 100644 mcp_servers/onboarding/default.md
 create mode 100644 test_discovery.py

diff --git a/mcp_browser/proxy.py b/mcp_browser/proxy.py
index fc00fea..5465b94 100644
--- a/mcp_browser/proxy.py
+++ b/mcp_browser/proxy.py
@@ -51,6 +51,9 @@ class MCPBrowser:
         self._response_buffer: Dict[Union[str, int], asyncio.Future] = {}
         self._next_id = 1
         self.logger = get_logger(__name__)
+        self._config_watcher = None
+        self._server_configs = {}
+        self._config_mtime = None
         
     async def __aenter__(self):
         """Async context manager entry."""
@@ -96,10 +99,18 @@ class MCPBrowser:
         # Initialize connection
         await self._initialize_connection()
         
+        # Start config file watcher
+        await self._start_config_watcher()
+        
+        # Store server configs for discovery
+        self._update_server_configs()
+        
         self._initialized = True
         
     async def close(self):
         """Close the browser and stop all MCP servers."""
+        if self._config_watcher:
+            self._config_watcher.cancel()
         if self.server:
             await self.server.stop()
         if self.multi_server:
@@ -394,6 +405,62 @@ class MCPBrowser:
         """Forward a request to the MCP server and get response."""
         # This is used by the virtual tool handler for mcp_call
         return await self.call(request)
+    
+    async def _start_config_watcher(self):
+        """Start watching the config file for changes."""
+        config_path = self.config_loader.config_path
+        if not config_path.exists():
+            return
+            
+        # Store initial mtime
+        self._config_mtime = config_path.stat().st_mtime
+        
+        async def watch_config():
+            """Watch for config file changes."""
+            while True:
+                try:
+                    await asyncio.sleep(2)  # Check every 2 seconds
+                    
+                    if not config_path.exists():
+                        continue
+                        
+                    current_mtime = config_path.stat().st_mtime
+                    if current_mtime != self._config_mtime:
+                        self.logger.info("Config file changed, reloading...")
+                        self._config_mtime = current_mtime
+                        
+                        # Reload config
+                        try:
+                            new_config = self.config_loader.load()
+                            self.config = new_config
+                            self._update_server_configs()
+                            self.logger.info("Config reloaded successfully")
+                        except Exception as e:
+                            self.logger.error(f"Failed to reload config: {e}")
+                            
+                except asyncio.CancelledError:
+                    break
+                except Exception as e:
+                    self.logger.error(f"Config watcher error: {e}")
+                    await asyncio.sleep(5)
+        
+        self._config_watcher = asyncio.create_task(watch_config())
+    
+    def _update_server_configs(self):
+        """Update server configurations for discovery."""
+        self._server_configs = {}
+        
+        if self.config and self.config.servers:
+            for name, server in self.config.servers.items():
+                self._server_configs[name] = {
+                    "name": name,
+                    "description": server.description or f"MCP server: {name}",
+                    "command": server.command,
+                    "available": True
+                }
+        
+        # Update registry metadata with server info
+        self.registry._metadata["servers"] = self._server_configs
 
 
 # Convenience function for simple usage
diff --git a/mcp_browser/registry.py b/mcp_browser/registry.py
index 30562bf..b9503d1 100644
--- a/mcp_browser/registry.py
+++ b/mcp_browser/registry.py
@@ -65,7 +65,8 @@ class ToolRegistry:
         search_data = {
             "tools": self.raw_tool_list,
             "tool_names": self.get_all_tool_names(),
-            "metadata": self._metadata
+            "metadata": self._metadata,
+            "servers": self._metadata.get("servers", {})
         }
         
         # Execute JSONPath query
@@ -85,11 +86,12 @@ class ToolRegistry:
         Returns only essential meta-tools for discovery.
         """
         tool_count = len(self.tools)
+        server_count = len(self._metadata.get("servers", {}))
         
         sparse_tools = [
             {
                 "name": "mcp_discover",
-                "description": f"Discover available tools using JSONPath. {tool_count} tools available.",
+                "description": f"Discover available tools and servers using JSONPath. {tool_count} tools from {server_count} servers available.",
                 "inputSchema": {
                     "type": "object",
                     "properties": {
diff --git a/mcp_servers/onboarding/default.md b/mcp_servers/onboarding/default.md
new file mode 100644
index 0000000..12dba93
--- /dev/null
+++ b/mcp_servers/onboarding/default.md
@@ -0,0 +1,90 @@
+# MCP Browser - Universal Model Context Protocol Proxy
+
+Welcome to MCP Browser! This tool acts as a proxy between AI systems and MCP servers, providing:
+
+## Core Capabilities
+
+### 1. **Proxy Mode**
+MCP Browser acts as a transparent proxy to external MCP servers configured in `~/.claude/mcp-browser/config.yaml`. You can:
+- Connect to any MCP server (filesystem, brave-search, github, etc.)
+- Add new servers at runtime without restarting
+- Access all tools from configured servers through the proxy
+
+### 2. **Built-in Tools**
+Always available, regardless of external servers:
+- **Screen Management** - Create/manage GNU screen sessions
+- **Memory & Tasks** - Persistent memory and task tracking
+- **Pattern Manager** - Auto-response patterns
+- **Onboarding** - Context-specific instructions (this tool)
+
+### 3. **Sparse Mode Optimization**
+To minimize context usage, only 3 meta-tools are shown initially:
+- `mcp_discover` - Discover all available tools using JSONPath
+- `mcp_call` - Execute any tool by constructing JSON-RPC calls
+- `onboarding` - Get/set identity-specific instructions
+
+## Discovery Examples
+
+```python
+# Discover all available tools (built-in + external servers)
+mcp_discover(jsonpath="$.tools[*].name")
+
+# Get tools from specific server
+mcp_discover(jsonpath="$.servers.brave-search.tools[*].name")
+
+# Get all configured servers
+mcp_discover(jsonpath="$.servers[*].name")
+
+# Get tool details
+mcp_discover(jsonpath="$.tools[?(@.name=='brave_web_search')]")
+```
+
+## Using External Server Tools
+
+Once discovered, call any tool through `mcp_call`:
+
+```python
+# Example: Brave search
+mcp_call(
+    method="tools/call",
+    params={
+        "name": "brave_web_search",
+        "arguments": {"query": "MCP protocol"}
+    }
+)
+
+# Example: GitHub
+mcp_call(
+    method="tools/call", 
+    params={
+        "name": "search_repositories",
+        "arguments": {"query": "mcp-browser"}
+    }
+)
+```
+
+## Runtime Configuration
+
+The config file at `~/.claude/mcp-browser/config.yaml` is monitored for changes. You can:
+1. Add new server configurations
+2. The proxy will automatically reload and make new tools available
+3. No restart required!
+
+Example config addition:
+```yaml
+servers:
+  github:
+    command: ["npx", "-y", "@modelcontextprotocol/server-github"]
+    env:
+      GITHUB_TOKEN: ${GITHUB_TOKEN}
+```
+
+## Architecture
+
+```
+Claude Desktop → MCP Browser (Proxy) → External MCP Servers
+                       ↓
+                 Built-in Servers
+```
+
+MCP Browser provides a unified interface to multiple MCP servers while optimizing context usage through sparse mode and discovery.
\ No newline at end of file
diff --git a/test_discovery.py b/test_discovery.py
new file mode 100644
index 0000000..37ae5ee
--- /dev/null
+++ b/test_discovery.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+"""Test the enhanced discovery 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_discovery():
+    """Test discovery with server information."""
+    browser = MCPBrowser(enable_builtin_servers=True)
+    
+    await browser.initialize()
+    
+    print("=== Testing Discovery ===\n")
+    
+    # Test 1: Discover all servers
+    print("1. Discovering all servers:")
+    servers = browser.discover("$.servers[*].name")
+    print(f"   Found {len(servers) if servers else 0} servers: {servers}\n")
+    
+    # Test 2: Get server details
+    print("2. Server details:")
+    server_info = browser.discover("$.servers")
+    if server_info:
+        for name, info in server_info.items():
+            print(f"   - {name}: {info.get('description', 'No description')}")
+    print()
+    
+    # Test 3: Discover tools with server count
+    print("3. Testing tools/list for sparse tools:")
+    response = await browser.call({
+        "jsonrpc": "2.0",
+        "id": 1,
+        "method": "tools/list"
+    })
+    
+    if "result" in response:
+        tools = response["result"]["tools"]
+        for tool in tools:
+            if tool["name"] == "mcp_discover":
+                print(f"   mcp_discover description: {tool['description']}")
+                break
+    
+    # Test 4: Onboarding content
+    print("\n4. Getting default onboarding:")
+    response = await browser.call({
+        "jsonrpc": "2.0",
+        "id": 2,
+        "method": "tools/call",
+        "params": {
+            "name": "onboarding",
+            "arguments": {"identity": "test-discovery"}
+        }
+    })
+    
+    if "result" in response:
+        content = response["result"]["content"][0]["text"]
+        # Just show first few lines
+        lines = content.split('\n')[:10]
+        print("   " + "\n   ".join(lines) + "\n   ...")
+    
+    await browser.close()
+
+
+if __name__ == "__main__":
+    asyncio.run(test_discovery())
\ No newline at end of file
