diff --git a/openmanus_server/README.md b/openmanus_server/README.md new file mode 100644 index 0000000..faf0459 --- /dev/null +++ b/openmanus_server/README.md @@ -0,0 +1,152 @@ +# OpenManus-server 🤖 + +This project provides a server based on [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) that exposes **OpenManus** tool functionalities as standardized APIs. + +## ✨ Features + +This MCP server provides access to the following OpenManus tools: + +1. **Browser Automation** 🌐 - Navigate webpages, click elements, input text, and more +2. **Google Search** 🔍 - Execute searches and retrieve result links +3. **Python Code Execution** 🐍 - Run Python code in a secure environment +4. **File Saving** 💾 - Save content to local files +5. **Termination Control** 🛑 - Control program execution flow + +## 🚀 Installation + +### Prerequisites + +- Python 3.10+ +- OpenManus project dependencies + +### Installation Steps + +1. First, install the OpenManus project: + +```bash +git clone https://github.com/mannaandpoem/OpenManus.git +cd OpenManus +``` + +2. Install dependencies: + +```bash +# Using uv (recommended) +curl -LsSf https://astral.sh/uv/install.sh | sh +uv venv +source .venv/bin/activate # Unix/macOS +# or .venv\Scripts\activate # Windows +uv sync +``` + +3. Install MCP dependencies: + +```bash +pip install mcp-python +``` + +## 📖 Usage + +### Starting the MCP Server + +The server supports two communication modes: stdio and HTTP. + +#### stdio mode (default) + +```bash +python mcp_server.py +``` + +#### HTTP mode + +```bash +python mcp_server.py --transport http --host 127.0.0.1 --port 8000 +``` + +### Command Line Arguments + +- `--transport`: Communication method, choose "stdio" or "http" (default: stdio) +- `--host`: HTTP server host address (default: 127.0.0.1) +- `--port`: HTTP server port (default: 8000) + +## 💻 Client Example + +Check out `mcp_client_example.py` to learn how to connect to the server and call tools using the MCP client. + +### Running the Client Example + +1. First, start the server in HTTP mode: + +```bash +python mcp_server.py --transport http +``` + +2. In another terminal, run the client example: + +```bash +python mcp_client_example.py +``` + +## 🤖 LLM Integration + +The MCP server can be integrated with LLMs that support tool calling, such as Claude 3 Opus/Sonnet/Haiku. + +### Example with Claude + +```python +import anthropic +from mcp.client import MCPClient + +# Initialize Claude client +client = anthropic.Anthropic(api_key="your_api_key") + +# Connect to MCP server +mcp_client = await MCPClient.create_http("http://localhost:8000") + +# Get tool definitions +tools = await mcp_client.list_tools() +tool_definitions = [tool.to_dict() for tool in tools] + +# Create Claude message +message = client.messages.create( + model="claude-3-opus-20240229", + max_tokens=1000, + temperature=0, + system="You are a helpful assistant that can use tools to help users.", + messages=[{"role": "user", "content": "Search for Model Context Protocol and summarize the top 3 results"}], + tools=tool_definitions +) + +# Handle tool calls +for tool_call in message.content: + if hasattr(tool_call, "tool_use"): + tool_name = tool_call.tool_use.name + tool_params = tool_call.tool_use.input + + # Call MCP tool + result = await mcp_client.invoke_tool(tool_name, tool_params) + + # Send results back to Claude + message = client.messages.create( + model="claude-3-opus-20240229", + max_tokens=1000, + temperature=0, + system="You are a helpful assistant that can use tools to help users.", + messages=[ + {"role": "user", "content": "Search for Model Context Protocol and summarize the top 3 results"}, + {"role": "assistant", "content": [tool_call]}, + {"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_call.tool_use.id, "content": result}]} + ], + tools=tool_definitions + ) +``` + +## 🔒 Security Considerations + +- By default, the HTTP server only listens on localhost (127.0.0.1) and is not exposed externally +- When using in production, ensure proper authentication and authorization mechanisms are in place +- The Python execution tool has timeout limits to prevent long-running code + +## 📄 License + +Same license as the OpenManus project \ No newline at end of file diff --git a/openmanus_server/mcp_requirements.txt b/openmanus_server/mcp_requirements.txt new file mode 100644 index 0000000..c9b19fc --- /dev/null +++ b/openmanus_server/mcp_requirements.txt @@ -0,0 +1,3 @@ +# Core dependencies +mcp +httpx>=0.27.0 \ No newline at end of file diff --git a/openmanus_server/openmanus_client_example.py b/openmanus_server/openmanus_client_example.py new file mode 100644 index 0000000..4287b4e --- /dev/null +++ b/openmanus_server/openmanus_client_example.py @@ -0,0 +1,174 @@ +import asyncio +from typing import Optional +from contextlib import AsyncExitStack +import os + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +class OpenManusClient: + def __init__(self): + # Initialize session and client objects + self.session: Optional[ClientSession] = None + self.exit_stack = AsyncExitStack() + self.stdio = None + self.write = None + + async def connect_to_server(self, server_script_path: str): + """Connect to an MCP server via stdio + + Args: + server_script_path: Path to the server script + """ + if not server_script_path.endswith('.py'): + raise ValueError("Server script must be a .py file") + + # Get the current directory to add to PYTHONPATH + current_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname(current_dir) # Get parent directory + + # Prepare environment variables + env = os.environ.copy() # Copy current environment + + # Add current directory and project root to PYTHONPATH + path_separator = ';' if os.name == 'nt' else ':' # Use ; for Windows, : for Unix + if 'PYTHONPATH' in env: + env['PYTHONPATH'] = f"{current_dir}{path_separator}{project_root}{path_separator}{env['PYTHONPATH']}" + else: + env['PYTHONPATH'] = f"{current_dir}{path_separator}{project_root}" + + server_params = StdioServerParameters( + command="python", + args=[server_script_path], + env=env + ) + + stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) + self.stdio, self.write = stdio_transport + self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) + await self.session.initialize() + + # List available tools + response = await self.session.list_tools() + tools = response.tools + print("\nConnected to server with tools:", [tool.name for tool in tools]) + return tools + + async def run_examples(self): + """Run example tool calls to demonstrate functionality""" + try: + print("\nExample 1: Google Search") + search_result = await self.session.call_tool( + "google_search", + {"query": "Model Context Protocol", "num_results": 5} + ) + print(f"Search results: {search_result.content}") + + print("\nExample 2: Python Code Execution") + code = """ +import math +result = 0 +for i in range(1, 10): + result += math.sqrt(i) +print(f"Calculation result: {result}") +""" + python_result = await self.session.call_tool( + "python_execute", + {"code": code, "timeout": 3} + ) + print(f"Python execution result: {python_result.content}") + + print("\nExample 3: File Saving") + file_result = await self.session.call_tool( + "file_saver", + { + "content": "This is a test file content saved through MCP", + "file_path": "mcp_test_file.txt" + } + ) + print(f"File save result: {file_result.content}") + + print("\nExample 4: Browser Usage") + # Navigate to webpage + browser_result = await self.session.call_tool( + "browser_use", + {"action": "navigate", "url": "https://www.example.com"} + ) + print(f"Browser navigation result: {browser_result.content}") + + # Get browser state + state_result = await self.session.call_tool( + "get_browser_state", + {} + ) + print(f"Browser state: {state_result.content}") + + except Exception as e: + print(f"\nError during example execution: {str(e)}") + + async def chat_loop(self): + """Run an interactive chat loop for testing tools""" + print("\nOpenManus MCP Client Started!") + print("Type your commands or 'quit' to exit.") + print("Available commands: google_search, python_execute, file_saver, browser_use, get_browser_state") + + while True: + try: + command = input("\nCommand: ").strip() + + if command.lower() == 'quit': + break + + # Parse command and parameters + parts = command.split(maxsplit=1) + if len(parts) == 0: + continue + + tool_name = parts[0] + tool_args = {} + if len(parts) > 1: + try: + tool_args = eval(parts[1]) # Convert string to dict + except: + print("Invalid arguments format. Please provide a valid Python dictionary.") + continue + + result = await self.session.call_tool(tool_name, tool_args) + print("\nResult:", result.content) + + except Exception as e: + print(f"\nError: {str(e)}") + + async def cleanup(self): + """Clean up resources""" + if self.session: + await self.session.close() + await self.exit_stack.aclose() + print("\nClosed MCP client connection") + +async def main(): + """Main entry point""" + import sys + + if len(sys.argv) < 2: + print("Usage: python openmanus_client_example.py ") + print("Example: python openmanus_client_example.py ../mcp_server.py") + sys.exit(1) + + client = OpenManusClient() + try: + await client.connect_to_server(server_script_path=sys.argv[1]) + + # Run examples first + await client.run_examples() + + # Then start interactive chat loop + await client.chat_loop() + + except Exception as e: + print(f"Error: {str(e)}") + finally: + await client.cleanup() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/openmanus_server/openmanus_server.py b/openmanus_server/openmanus_server.py new file mode 100644 index 0000000..023d53d --- /dev/null +++ b/openmanus_server/openmanus_server.py @@ -0,0 +1,177 @@ +from typing import Optional +import asyncio +import json +import argparse +from mcp.server.fastmcp import FastMCP +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger("mcp-server") + +# Import OpenManus tools +from app.tool.browser_use_tool import BrowserUseTool +from app.tool.google_search import GoogleSearch +from app.tool.python_execute import PythonExecute +from app.tool.file_saver import FileSaver +from app.tool.terminate import Terminate + +# Initialize FastMCP server +openmanus = FastMCP("openmanus") + +# Initialize tool instances +browser_tool = BrowserUseTool() +google_search_tool = GoogleSearch() +python_execute_tool = PythonExecute() +file_saver_tool = FileSaver() +terminate_tool = Terminate() + +# Browser tool +@openmanus.tool() +async def browser_use( + action: str, + url: Optional[str] = None, + index: Optional[int] = None, + text: Optional[str] = None, + script: Optional[str] = None, + scroll_amount: Optional[int] = None, + tab_id: Optional[int] = None, +) -> str: + """Execute various browser operations. + + Args: + action: The browser operation to execute, possible values include: + - navigate: Navigate to specified URL + - click: Click on an element on the page + - input_text: Input text into a text field + - screenshot: Take a screenshot of the current page + - get_html: Get HTML of the current page + - get_text: Get text content of the current page + - execute_js: Execute JavaScript code + - scroll: Scroll the page + - switch_tab: Switch to specified tab + - new_tab: Open new tab + - close_tab: Close current tab + - refresh: Refresh current page + url: URL for 'navigate' or 'new_tab' operations + index: Element index for 'click' or 'input_text' operations + text: Text for 'input_text' operation + script: JavaScript code for 'execute_js' operation + scroll_amount: Scroll pixels for 'scroll' operation (positive for down, negative for up) + tab_id: Tab ID for 'switch_tab' operation + """ + logger.info(f"Executing browser operation: {action}") + result = await browser_tool.execute( + action=action, + url=url, + index=index, + text=text, + script=script, + scroll_amount=scroll_amount, + tab_id=tab_id + ) + return json.dumps(result.model_dump()) + +@openmanus.tool() +async def get_browser_state() -> str: + """Get current browser state, including URL, title, tabs and interactive elements.""" + logger.info("Getting browser state") + result = await browser_tool.get_current_state() + return json.dumps(result.model_dump()) + +# Google search tool +@openmanus.tool() +async def google_search(query: str, num_results: int = 10) -> str: + """Execute Google search and return list of relevant links. + + Args: + query: Search query + num_results: Number of results to return (default is 10) + """ + logger.info(f"Executing Google search: {query}") + results = await google_search_tool.execute(query=query, num_results=num_results) + return json.dumps(results) + +# Python execution tool +@openmanus.tool() +async def python_execute(code: str, timeout: int = 5) -> str: + """Execute Python code and return results. + + Args: + code: Python code to execute + timeout: Execution timeout in seconds + """ + logger.info("Executing Python code") + result = await python_execute_tool.execute(code=code, timeout=timeout) + return json.dumps(result) + +# File saver tool +@openmanus.tool() +async def file_saver(content: str, file_path: str, mode: str = "w") -> str: + """Save content to local file. + + Args: + content: Content to save + file_path: File path + mode: File open mode (default is 'w') + """ + logger.info(f"Saving file: {file_path}") + result = await file_saver_tool.execute(content=content, file_path=file_path, mode=mode) + return result + +# Terminate tool +@openmanus.tool() +async def terminate(status: str) -> str: + """Terminate program execution. + + Args: + status: Termination status, can be 'success' or 'failure' + """ + logger.info(f"Terminating execution: {status}") + result = await terminate_tool.execute(status=status) + return result + +# Clean up resources +async def cleanup(): + """Clean up all tool resources""" + logger.info("Cleaning up resources") + await browser_tool.cleanup() + +# Register cleanup function +import atexit +atexit.register(lambda: asyncio.run(cleanup())) + +def parse_args(): + """Parse command line arguments""" + parser = argparse.ArgumentParser(description="OpenManus MCP Server") + parser.add_argument( + "--transport", + choices=["stdio", "http"], + default="stdio", + help="Communication method: stdio or http (default: stdio)" + ) + parser.add_argument( + "--host", + default="127.0.0.1", + help="HTTP server host (default: 127.0.0.1)" + ) + parser.add_argument( + "--port", + type=int, + default=8000, + help="HTTP server port (default: 8000)" + ) + return parser.parse_args() + +if __name__ == "__main__": + args = parse_args() + + if args.transport == "stdio": + logger.info("Starting OpenManus server (stdio mode)") + openmanus.run(transport="stdio") + else: + logger.info(f"Starting OpenManus server (HTTP mode) at {args.host}:{args.port}") + openmanus.run(transport="http", host=args.host, port=args.port) \ No newline at end of file