diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0bdcccc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,122 @@ +# CLAUDE.md + +## šŸ¤– AI-Only Development Notice + +This repository is developed and maintained exclusively by AI assistants. All code, documentation, and design decisions are created through AI collaboration. + +## Project Overview + +MCP Browser is a generic, minimalistic Model Context Protocol (MCP) browser designed specifically for AI systems to interact with MCP servers while optimizing context usage. + +### Key Design Principles + +1. **Context Optimization First**: Every design decision prioritizes minimal token usage +2. **Generic Interface**: No hardcoded tool knowledge - pure protocol implementation +3. **Sparse Mode**: Initially expose only 3 tools to minimize context bloat +4. **AI-Friendly API**: Simple `call()` and `discover()` methods for all operations + +## For AI Developers + +When working on this codebase: + +### Getting Started +```bash +# Generate AI-friendly documentation +python setup.py aidocs + +# This creates: +# - docs/STRUCTURE.md - Project structure overview +# - docs/API_SUMMARY.md - API quick reference +# - .tags - ctags for code navigation +# - HTML documentation via pydoc +``` + +### Core Concepts + +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 +4. **Identity-Aware**: Onboarding tool accepts identity parameter for context-specific instructions + +### Architecture Overview + +``` +mcp_browser/ +ā”œā”€ā”€ proxy.py # Main MCPBrowser class - entry point +ā”œā”€ā”€ registry.py # Tool storage and JSONPath discovery +ā”œā”€ā”€ filter.py # Sparse mode implementation +ā”œā”€ā”€ multi_server.py # Manages multiple MCP servers +└── server.py # Individual MCP server wrapper + +mcp_servers/ +ā”œā”€ā”€ base.py # Base class for Python MCP servers +ā”œā”€ā”€ screen/ # GNU screen session management +ā”œā”€ā”€ memory/ # Persistent memory and tasks +ā”œā”€ā”€ patterns/ # Auto-response patterns +└── onboarding/ # Identity-aware onboarding +``` + +### Development Workflow + +1. **Always read existing code first** - Use patterns from existing implementations +2. **Test with examples** - Run `examples/builtin_servers_demo.py` to verify changes +3. **Maintain sparse mode** - Don't expose tools unnecessarily +4. **Document for AI** - Comments should help future AI understand intent + +### Testing MCP Connections + +```python +# Basic test +from mcp_browser import MCPBrowser + +async with MCPBrowser() as browser: + # List sparse tools (only 3 visible) + response = await browser.call({ + "jsonrpc": "2.0", + "method": "tools/list" + }) + + # Discover all tools + all_tools = browser.discover("$.tools[*].name") +``` + +### Common Tasks + +**Adding a new built-in server:** +1. Create `mcp_servers/newserver/newserver_server.py` +2. Inherit from `BaseMCPServer` +3. Register tools in `__init__` +4. Add to `MultiServerManager.builtin_servers` + +**Modifying sparse tools:** +1. Edit `ToolRegistry.get_sparse_tools()` +2. Ensure tool descriptions are concise +3. Test that only intended tools appear initially + +## Design Rationale + +- **Why sparse mode?** - Claude and other LLMs have limited context windows. Hundreds of tool descriptions can consume significant tokens. +- **Why generic design?** - Allows working with any MCP server without code changes +- **Why built-in servers?** - Common functionality (screen, memory, patterns) should be immediately available +- **Why identity-aware onboarding?** - Different AI instances/projects need different context + +## Protocol Details + +MCP uses JSON-RPC 2.0 over stdin/stdout. Key methods: +- `initialize` - Handshake and capability negotiation +- `tools/list` - Get available tools +- `tools/call` - Execute a tool +- `notifications/*` - Server-initiated messages + +## Future AI Instructions + +When enhancing this codebase: +- Maintain backward compatibility with the 2-method API +- Keep sparse mode as default behavior +- Document any new virtual tools clearly +- Test with multiple MCP server implementations +- Consider token usage in all decisions + +--- + +*This project demonstrates AI-first development where context efficiency and generic abstractions enable powerful capabilities with minimal complexity.* \ No newline at end of file diff --git a/mcp_browser/__main__.py b/mcp_browser/__main__.py new file mode 100644 index 0000000..dfa9d49 --- /dev/null +++ b/mcp_browser/__main__.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +""" +MCP Browser command-line interface. +""" + +import sys +import asyncio +import argparse +import json +from pathlib import Path + +from .proxy import MCPBrowser + + +async def interactive_mode(browser: MCPBrowser): + """Run MCP Browser in interactive mode.""" + print("MCP Browser Interactive Mode") + print("Type 'help' for commands, 'exit' to quit\n") + + while True: + try: + command = input("> ").strip() + + if command == "exit": + break + elif command == "help": + print("\nCommands:") + 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(" exit - Exit interactive mode") + print("\nExamples:") + print(' discover $.tools[*].name') + print(' call {"method": "tools/list"}') + print(' onboard MyProject') + continue + + elif command.startswith("discover "): + jsonpath = command[9:] + result = browser.discover(jsonpath) + print(json.dumps(result, indent=2)) + + elif command.startswith("call "): + json_str = command[5:] + request = json.loads(json_str) + if "jsonrpc" not in request: + request["jsonrpc"] = "2.0" + response = await browser.call(request) + print(json.dumps(response, indent=2)) + + elif command.startswith("onboard "): + identity = command[8:] + response = await browser.call({ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "onboarding", + "arguments": {"identity": identity} + } + }) + if "result" in response: + print(response["result"]["content"][0]["text"]) + + elif command == "list": + response = await browser.call({ + "jsonrpc": "2.0", + "method": "tools/list" + }) + if "result" in response: + tools = response["result"]["tools"] + for tool in tools: + print(f"- {tool['name']}: {tool['description']}") + + else: + print("Unknown command. Type 'help' for available commands.") + + except KeyboardInterrupt: + print("\nUse 'exit' to quit") + except Exception as e: + print(f"Error: {e}") + + +async def run_server_mode(browser: MCPBrowser): + """Run MCP Browser as an MCP server (stdin/stdout).""" + import sys + + await browser.initialize() + + # Read JSON-RPC from stdin, write to stdout + buffer = "" + while True: + try: + chunk = sys.stdin.read(4096) + if not chunk: + break + + buffer += chunk + while '\n' in buffer: + line, buffer = buffer.split('\n', 1) + if line.strip(): + try: + request = json.loads(line) + response = await browser.call(request) + print(json.dumps(response)) + sys.stdout.flush() + except json.JSONDecodeError: + pass + + except KeyboardInterrupt: + break + except EOFError: + break + + +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.add_argument("--mode", choices=["interactive", "server"], + default="interactive", help="Operating mode") + parser.add_argument("--no-sparse", action="store_true", + help="Disable sparse mode") + parser.add_argument("--no-builtin", action="store_true", + help="Disable built-in servers") + + args = parser.parse_args() + + # Create browser + browser = MCPBrowser( + server_name=args.server, + config_path=args.config, + sparse_mode=not args.no_sparse, + enable_builtin=not args.no_builtin + ) + + # Run in appropriate mode + if args.mode == "server": + asyncio.run(run_server_mode(browser)) + else: + asyncio.run(async_main(browser)) + + +async def async_main(browser: MCPBrowser): + """Async main for interactive mode.""" + try: + await browser.initialize() + await interactive_mode(browser) + finally: + await browser.close() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/mcp_browser/py.typed b/mcp_browser/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index 0b607f1..5c732d4 100644 --- a/setup.py +++ b/setup.py @@ -1,41 +1,179 @@ -"""Setup configuration for MCP Browser.""" +""" +Setup script for MCP Browser. -from setuptools import setup, find_packages +This package is developed exclusively by AI assistants. +""" +from setuptools import setup, find_packages, Command +import os +import sys +import subprocess +from pathlib import Path + + +class GenerateAIDocs(Command): + """Generate AI-friendly documentation.""" + description = 'Generate documentation for AI navigation' + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + """Run all AI documentation generators.""" + print("Generating AI-friendly documentation...") + + # 1. Generate API documentation + try: + subprocess.run([sys.executable, '-m', 'pydoc', '-w', '.'], + cwd='mcp_browser', check=True) + print("āœ“ Generated pydoc API documentation") + except Exception as e: + print(f"⚠ pydoc generation failed: {e}") + + # 2. Generate ctags for code navigation + try: + subprocess.run(['ctags', '-R', '--languages=Python', + '--python-kinds=-i', '-f', '.tags'], check=True) + print("āœ“ Generated ctags file") + except FileNotFoundError: + print("⚠ ctags not installed (install with: apt-get install universal-ctags)") + except Exception as e: + print(f"⚠ ctags generation failed: {e}") + + # 3. Generate structure documentation + self.generate_structure_doc() + + # 4. Generate API summary + self.generate_api_summary() + + print("\nAI documentation generation complete!") + print("Files created:") + print(" - docs/STRUCTURE.md - Project structure overview") + print(" - docs/API_SUMMARY.md - API quick reference") + print(" - .tags - ctags for code navigation") + print(" - *.html - pydoc HTML documentation") + + def generate_structure_doc(self): + """Generate project structure documentation.""" + structure = [] + for root, dirs, files in os.walk('.'): + # Skip hidden directories and __pycache__ + dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__'] + + level = root.replace('.', '').count(os.sep) + indent = ' ' * level + structure.append(f"{indent}{os.path.basename(root)}/") + + subindent = ' ' * (level + 1) + for file in sorted(files): + if file.endswith('.py'): + structure.append(f"{subindent}{file}") + + os.makedirs('docs', exist_ok=True) + with open('docs/STRUCTURE.md', 'w') as f: + f.write("# Project Structure\n\n") + f.write("```\n") + f.write('\n'.join(structure)) + f.write("\n```\n") + + def generate_api_summary(self): + """Generate API summary for quick reference.""" + api_summary = [] + api_summary.append("# MCP Browser API Summary\n") + api_summary.append("## Main Classes\n") + + # Extract main classes and methods + main_files = [ + ('mcp_browser/proxy.py', 'MCPBrowser'), + ('mcp_browser/registry.py', 'ToolRegistry'), + ('mcp_browser/server.py', 'MCPServer'), + ('mcp_browser/multi_server.py', 'MultiServerManager'), + ] + + for file_path, class_name in main_files: + if os.path.exists(file_path): + api_summary.append(f"\n### {class_name} ({file_path})\n") + # Simple method extraction (could be enhanced) + try: + with open(file_path, 'r') as f: + content = f.read() + lines = content.split('\n') + for i, line in enumerate(lines): + if line.strip().startswith('def ') and not line.strip().startswith('def _'): + method = line.strip()[4:].split('(')[0] + api_summary.append(f"- `{method}()`") + except Exception: + pass + + os.makedirs('docs', exist_ok=True) + with open('docs/API_SUMMARY.md', 'w') as f: + f.write('\n'.join(api_summary)) + + +# Read long description with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() + setup( name="mcp-browser", version="0.1.0", - author="MCP Browser Contributors", - description="A generic, minimalistic MCP protocol interface for AI systems", + description="A generic MCP browser with context optimization for AI systems", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/yourusername/mcp-browser", + author="AI Assistant", + author_email="ai@anthropic.com", + url="https://github.com/anthropics/mcp-browser", packages=find_packages(), + package_data={ + 'mcp_browser': ['py.typed'], + 'config': ['*.yaml'], + }, + install_requires=[ + "aiofiles>=23.0.0", + "jsonpath-ng>=1.6.0", + "pyyaml>=6.0", + "typing-extensions>=4.0.0;python_version<'3.11'", + ], + extras_require={ + 'dev': [ + 'pytest>=7.0.0', + 'pytest-asyncio>=0.21.0', + 'black>=23.0.0', + 'mypy>=1.0.0', + 'ruff>=0.1.0', + ], + 'docs': [ + 'sphinx>=6.0.0', + 'sphinx-rtd-theme>=1.3.0', + 'myst-parser>=2.0.0', + ], + }, + python_requires=">=3.8", + entry_points={ + "console_scripts": [ + "mcp-browser=mcp_browser.__main__:main", + "mcp-browser-demo=examples.basic_usage:main", + ], + }, + cmdclass={ + 'aidocs': GenerateAIDocs, + }, classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries :: Application Frameworks", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], - python_requires=">=3.8", - install_requires=[ - "pyyaml>=6.0", - "jsonpath-ng>=1.5.3", - ], - extras_require={ - "dev": [ - "pytest>=7.0", - "pytest-asyncio>=0.20", - "black>=22.0", - "mypy>=0.990", - ] - } + keywords="mcp model-context-protocol ai llm tools json-rpc", ) \ No newline at end of file diff --git a/test_claude_connection.py b/test_claude_connection.py new file mode 100644 index 0000000..84f15dd --- /dev/null +++ b/test_claude_connection.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Test MCP Browser connection to claude-code. + +This script tests if MCP Browser can successfully connect to claude-code +as an MCP target and perform basic operations like reading a file. +""" + +import asyncio +import sys +import os +from pathlib import Path + +# Add parent directory to path for development +sys.path.insert(0, str(Path(__file__).parent)) + +from mcp_browser import MCPBrowser + + +async def test_claude_connection(): + """Test connection to claude-code via MCP.""" + + print("=== Testing MCP Browser Connection to Claude Code ===\n") + + # Check if claude binary exists + claude_path = "/usr/local/bin/claude" + if not os.path.exists(claude_path): + print(f"āŒ Claude binary not found at {claude_path}") + print("Please ensure Claude Code is installed") + return + + print(f"āœ“ Found Claude binary at {claude_path}\n") + + # Create browser configured for claude-code + print("Creating MCP Browser with claude-code as target...") + + # We'll create a custom config for claude + browser = MCPBrowser( + server_command=[claude_path, "mcp"], + server_name="claude-code", + sparse_mode=True # Use sparse mode to minimize context + ) + + try: + # Initialize connection + print("Initializing MCP connection...") + await browser.initialize() + print("āœ“ Connected to claude-code via MCP\n") + + # Test 1: List available tools in sparse mode + print("1. Testing sparse mode tools:") + response = await browser.call({ + "jsonrpc": "2.0", + "id": "test-1", + "method": "tools/list" + }) + + if "result" in response: + tools = response["result"]["tools"] + print(f" Sparse tools available: {len(tools)}") + for tool in tools[:5]: # Show first 5 + print(f" - {tool['name']}: {tool['description'][:60]}...") + + # Test 2: Discover all tools + print("\n2. Discovering all available tools:") + all_tools = browser.discover("$.tools[*].name") + if all_tools: + print(f" Total tools discovered: {len(all_tools)}") + print(" Sample tools:", all_tools[:10]) + + # Test 3: Try to read a file using claude's Read tool + print("\n3. Testing file read capability:") + test_file = "/tmp/mcp_test.txt" + + # First create a test file + with open(test_file, 'w') as f: + f.write("Hello from MCP Browser!\nThis file was created to test claude-code integration.") + print(f" Created test file: {test_file}") + + # Use mcp_call to invoke Read tool + response = await browser.call({ + "jsonrpc": "2.0", + "id": "test-3", + "method": "tools/call", + "params": { + "name": "mcp_call", + "arguments": { + "method": "tools/call", + "params": { + "name": "Read", # Claude's Read tool + "arguments": { + "file_path": test_file + } + } + } + } + }) + + if "result" in response: + print(" āœ“ Successfully read file via claude-code!") + content = response["result"]["content"][0]["text"] + print(f" File content preview: {content[:100]}...") + else: + print(" āŒ Failed to read file:", response.get("error", "Unknown error")) + + # Test 4: Use onboarding + print("\n4. Testing identity-aware onboarding:") + response = await browser.call({ + "jsonrpc": "2.0", + "id": "test-4", + "method": "tools/call", + "params": { + "name": "onboarding", + "arguments": { + "identity": "ClaudeCodeTest", + "instructions": "Remember: You're testing MCP Browser integration with claude-code" + } + } + }) + + if "result" in response: + print(" āœ“ Onboarding set successfully") + + # Clean up test file + os.remove(test_file) + + print("\nāœ… All tests completed successfully!") + print("\nMCP Browser can successfully:") + print("- Connect to claude-code as an MCP server") + print("- List and discover available tools") + print("- Execute claude-code tools (like Read)") + print("- Use built-in features (onboarding)") + + except Exception as e: + print(f"\nāŒ Error during testing: {e}") + import traceback + traceback.print_exc() + + finally: + print("\nClosing connection...") + await browser.close() + print("āœ“ Connection closed") + + +if __name__ == "__main__": + print("\nNote: This test requires claude-code to be installed at /usr/local/bin/claude") + print("If claude is installed elsewhere, update the path in the script.\n") + + asyncio.run(test_claude_connection()) \ No newline at end of file