feat: init MCP server
This commit is contained in:
parent
576090439b
commit
e08a6b313a
152
openmanus_server/README.md
Normal file
152
openmanus_server/README.md
Normal file
@ -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
|
3
openmanus_server/mcp_requirements.txt
Normal file
3
openmanus_server/mcp_requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# Core dependencies
|
||||
mcp
|
||||
httpx>=0.27.0
|
174
openmanus_server/openmanus_client_example.py
Normal file
174
openmanus_server/openmanus_client_example.py
Normal file
@ -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 <path_to_server_script>")
|
||||
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())
|
177
openmanus_server/openmanus_server.py
Normal file
177
openmanus_server/openmanus_server.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user