diff --git a/Dockerfile b/Dockerfile index 9f7a19081..3417f4ae9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,12 +2,34 @@ FROM python:3.12-slim WORKDIR /app/OpenManus -RUN apt-get update && apt-get install -y --no-install-recommends git curl \ +# Install system deps including Playwright browser requirements +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl wget gnupg ca-certificates \ + libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 \ + libdrm2 libdbus-1-3 libexpat1 libxcb1 libxkbcommon0 libx11-6 \ + libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 \ + libgbm1 libpango-1.0-0 libcairo2 libasound2 libatspi2.0-0 \ && rm -rf /var/lib/apt/lists/* \ && (command -v uv >/dev/null 2>&1 || pip install --no-cache-dir uv) COPY . . +# Install Python dependencies RUN uv pip install --system -r requirements.txt -CMD ["bash"] +# Install Daytona stub package (satisfies imports without real Daytona SDK) +# The real Daytona SDK is not on PyPI; this stub allows all agents to load. +# Sandbox operations will raise NotImplementedError if called without a real key. +RUN pip install --no-cache-dir ./daytona_stub/ + +# Install Playwright Chromium browser binaries +RUN playwright install chromium + +# Create workspace directory for agent file output (Railway Volume mounts here) +RUN mkdir -p /app/OpenManus/workspace + +# Expose default port (Railway overrides via $PORT env var at runtime) +EXPOSE 8000 + +# Start the MCP server in SSE (web) mode +CMD ["python", "run_mcp_server.py", "--transport", "sse"] diff --git a/app/config.py b/app/config.py index a881e2a5e..8b7fe3e81 100644 --- a/app/config.py +++ b/app/config.py @@ -1,4 +1,6 @@ import json +import os +import re import threading import tomllib from pathlib import Path @@ -106,7 +108,7 @@ class SandboxSettings(BaseModel): class DaytonaSettings(BaseModel): - daytona_api_key: str + daytona_api_key: Optional[str] = Field(None, description="Daytona API key (optional, only needed for sandbox mode)") daytona_server_url: Optional[str] = Field( "https://app.daytona.io/api", description="" ) @@ -227,8 +229,15 @@ def _get_config_path() -> Path: def _load_config(self) -> dict: config_path = self._get_config_path() - with config_path.open("rb") as f: - return tomllib.load(f) + # Read as text first so we can substitute ${VAR} placeholders from env + with config_path.open("r", encoding="utf-8") as f: + content = f.read() + # Replace all ${VAR_NAME} placeholders with environment variable values + for placeholder in re.findall(r"\$\{([^}]+)\}", content): + env_value = os.getenv(placeholder) + if env_value is not None: + content = content.replace(f"${{{placeholder}}}", env_value) + return tomllib.loads(content) def _load_initial_config(self): raw_config = self._load_config() diff --git a/app/mcp/agent_tool.py b/app/mcp/agent_tool.py new file mode 100644 index 000000000..34511968c --- /dev/null +++ b/app/mcp/agent_tool.py @@ -0,0 +1,117 @@ +""" +AgentTool: Wraps any OpenManus agent as a stateless, MCP-compatible tool. + +Each call creates a fresh agent instance, runs it to completion, and cleans up. +This ensures thread safety and predictable concurrent operation. + +Key design decision: `parameters` is a plain dict field (not a @property) so +that Pydantic's to_param() correctly serializes it instead of returning the +property descriptor object. Using @property causes Pydantic to return the +descriptor itself rather than calling it, resulting in: + AttributeError: 'property' object has no attribute 'get' +""" +import asyncio +from typing import Any, Type + +from pydantic import Field + +from app.logger import logger +from app.tool.base import BaseTool, ToolResult + + +# The parameters schema for all AgentTool instances - a single string prompt. +_AGENT_TOOL_PARAMETERS = { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": ( + "The natural language task description for the agent to execute. " + "Be as specific and detailed as possible." + ), + } + }, + "required": ["prompt"], +} + + +class AgentTool(BaseTool): + """ + A generic wrapper that exposes an OpenManus agent as a single-call MCP tool. + + The agent is instantiated fresh on every execute() call, ensuring complete + isolation between requests. This is the "Agent-as-a-Tool" pattern. + """ + + # Store agent_class as a plain field (not Pydantic-managed) to avoid + # issues with non-serializable types. + agent_class: Any = Field(default=None, exclude=True) + agent_config: dict = Field(default_factory=dict, exclude=True) + + # parameters MUST be a plain dict field (NOT a @property) so that + # BaseTool.to_param() serializes it correctly via self.parameters. + # Pydantic returns the property descriptor object instead of calling it, + # which breaks the MCP server's docstring/signature building. + parameters: dict = Field(default_factory=lambda: _AGENT_TOOL_PARAMETERS.copy()) + + class Config: + arbitrary_types_allowed = True + + def __init__(self, agent_class: Type, **agent_config): + agent_name = getattr(agent_class, "name", agent_class.__name__).lower() + # Replace spaces/dashes for a clean tool name + tool_name = f"run_{agent_name.replace('-', '_').replace(' ', '_')}" + agent_description = getattr(agent_class, "description", "An OpenManus agent.") + + super().__init__( + name=tool_name, + description=( + f"{agent_description}\n\n" + f"Runs the {agent_class.__name__} agent autonomously to completion. " + f"Provide a detailed natural language prompt describing the task." + ), + parameters=_AGENT_TOOL_PARAMETERS.copy(), + ) + # Store via object.__setattr__ to bypass pydantic immutability + object.__setattr__(self, "agent_class", agent_class) + object.__setattr__(self, "agent_config", agent_config) + + async def execute(self, prompt: str, **kwargs) -> ToolResult: + """ + Creates a fresh agent instance, runs it with the given prompt, + and ensures cleanup regardless of success or failure. + """ + agent_class = object.__getattribute__(self, "agent_class") + agent_config = object.__getattribute__(self, "agent_config") + + logger.info( + f"[AgentTool] Spawning {agent_class.__name__} for prompt: {prompt[:80]}..." + ) + agent = None + try: + # Use async factory .create() if available (e.g., Manus), else direct init + if hasattr(agent_class, "create") and asyncio.iscoroutinefunction( + agent_class.create + ): + agent = await agent_class.create(**agent_config) + else: + agent = agent_class(**agent_config) + + result = await agent.run(prompt) + logger.info(f"[AgentTool] {agent_class.__name__} completed successfully.") + return ToolResult(output=result) + + except Exception as e: + logger.error( + f"[AgentTool] {agent_class.__name__} failed: {e}", exc_info=True + ) + return ToolResult(output=f"Agent execution failed: {str(e)}", error=str(e)) + + finally: + if agent is not None and hasattr(agent, "cleanup"): + try: + await agent.cleanup() + except Exception as cleanup_err: + logger.warning( + f"[AgentTool] Cleanup failed for {agent_class.__name__}: {cleanup_err}" + ) diff --git a/app/mcp/server.py b/app/mcp/server.py index 3ee8b08c9..7a8e161e3 100644 --- a/app/mcp/server.py +++ b/app/mcp/server.py @@ -1,65 +1,178 @@ -import logging -import sys - - -logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler(sys.stderr)]) - import argparse import asyncio import atexit import json +import logging +import os +import sys from inspect import Parameter, Signature from typing import Any, Dict, Optional +import uvicorn from mcp.server.fastmcp import FastMCP +from starlette.applications import Starlette +from starlette.middleware import Middleware +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.routing import Mount, Route + + +# Configure logging early so any import-time messages are captured. +logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler(sys.stderr)]) + +# --------------------------------------------------------------------------- +# NOTE: Heavy imports (app.logger, agents, tools) are deferred to avoid +# slowing down the Starlette/uvicorn startup. Railway's healthcheck fires +# within seconds of the process starting, so the /health route must be +# reachable before any LLM config or agent initialization occurs. +# --------------------------------------------------------------------------- + + +# --------------------------------------------------------------------------- +# Auth Middleware +# --------------------------------------------------------------------------- + + +class BearerAuthMiddleware(BaseHTTPMiddleware): + """ + Validates Bearer token on the /sse endpoint. + If MCP_SERVER_AUTH_TOKEN is not set, auth is disabled (dev mode). + """ + + async def dispatch(self, request: Request, call_next): + auth_token = os.getenv("MCP_SERVER_AUTH_TOKEN") + + # Only enforce auth if the token is configured + if auth_token and request.url.path == "/sse": + auth_header = request.headers.get("Authorization", "") + if not auth_header: + return JSONResponse( + {"error": "Missing Authorization header"}, status_code=401 + ) + parts = auth_header.split() + if ( + len(parts) != 2 + or parts[0].lower() != "bearer" + or parts[1] != auth_token + ): + return JSONResponse( + {"error": "Invalid authentication credentials"}, status_code=401 + ) + + return await call_next(request) + + +# --------------------------------------------------------------------------- +# Health Check - must be reachable IMMEDIATELY on startup +# --------------------------------------------------------------------------- + +_server_ready = False -from app.logger import logger -from app.tool.base import BaseTool -from app.tool.bash import Bash -from app.tool.browser_use_tool import BrowserUseTool -from app.tool.str_replace_editor import StrReplaceEditor -from app.tool.terminate import Terminate + +async def health_check(request: Request) -> JSONResponse: + """Simple health endpoint for Railway's deployment checks.""" + return JSONResponse( + { + "status": "ok", + "service": "openmanus-mcp", + "ready": _server_ready, + } + ) + + +# --------------------------------------------------------------------------- +# MCP Server +# --------------------------------------------------------------------------- class MCPServer: - """MCP Server implementation with tool registration and management.""" + """ + OpenManus MCP Server. - def __init__(self, name: str = "openmanus"): - self.server = FastMCP(name) - self.tools: Dict[str, BaseTool] = {} + Exposes two layers of tools: + 1. High-level Agent Tools - run_manus, run_data_analysis, run_swe, run_browser + Each spawns a fresh, isolated agent instance per call for full autonomy. + 2. Low-level Primitive Tools - bash, str_replace_editor, terminate + Direct tool access for callers that need fine-grained control. - # Initialize standard tools - self.tools["bash"] = Bash() - self.tools["browser"] = BrowserUseTool() - self.tools["editor"] = StrReplaceEditor() - self.tools["terminate"] = Terminate() + Agent and tool imports are deferred until register_all_tools() is called + so that the HTTP server (and /health endpoint) can start immediately. + """ - def register_tool(self, tool: BaseTool, method_name: Optional[str] = None) -> None: + def __init__(self, name: str = "openmanus"): + port = int(os.getenv("PORT", os.getenv("FASTMCP_PORT", "8000"))) + self.server = FastMCP(name, port=port) + self.tools: Dict[str, Any] = {} + + def _load_tools(self) -> None: + """Deferred import and instantiation of all tools and agents. + + Uses try/except for each agent so that optional dependencies (e.g. daytona) + don't prevent the server from starting. Missing agents are skipped gracefully. + """ + import importlib + import logging as _logging + + _log = _logging.getLogger(__name__) + self._logger = _log + + from app.mcp.agent_tool import AgentTool + + # --- High-level: Agent-as-a-Tool (stateless, isolated per call) --- + agent_map = { + "run_manus": ("app.agent.manus", "Manus"), + "run_data_analysis": ("app.agent.data_analysis", "DataAnalysis"), + "run_swe": ("app.agent.swe", "SWEAgent"), + "run_browser": ("app.agent.browser", "BrowserAgent"), + } + for tool_name, (module_path, class_name) in agent_map.items(): + try: + mod = importlib.import_module(module_path) + agent_class = getattr(mod, class_name) + self.tools[tool_name] = AgentTool(agent_class=agent_class) + _log.info(f"Registered agent tool: {tool_name}") + except Exception as e: + _log.warning(f"Skipping agent tool '{tool_name}': {e}") + + # --- Low-level: Primitive tools for direct control --- + primitive_map = { + "bash": ("app.tool.bash", "Bash"), + "editor": ("app.tool.str_replace_editor", "StrReplaceEditor"), + "terminate": ("app.tool.terminate", "Terminate"), + } + for tool_name, (module_path, class_name) in primitive_map.items(): + try: + mod = importlib.import_module(module_path) + tool_class = getattr(mod, class_name) + self.tools[tool_name] = tool_class() + _log.info(f"Registered primitive tool: {tool_name}") + except Exception as e: + _log.warning(f"Skipping primitive tool '{tool_name}': {e}") + + _log.info(f"Loaded {len(self.tools)} tools: {list(self.tools.keys())}") + + def register_tool(self, tool: Any, method_name: Optional[str] = None) -> None: """Register a tool with parameter validation and documentation.""" + logger = self._logger tool_name = method_name or tool.name tool_param = tool.to_param() tool_function = tool_param["function"] - # Define the async function to be registered async def tool_method(**kwargs): logger.info(f"Executing {tool_name}: {kwargs}") result = await tool.execute(**kwargs) - logger.info(f"Result of {tool_name}: {result}") - - # Handle different types of results (match original logic) if hasattr(result, "model_dump"): return json.dumps(result.model_dump()) elif isinstance(result, dict): return json.dumps(result) return result - # Set method metadata tool_method.__name__ = tool_name tool_method.__doc__ = self._build_docstring(tool_function) tool_method.__signature__ = self._build_signature(tool_function) - # Store parameter schema (important for tools that access it programmatically) param_props = tool_function.get("parameters", {}).get("properties", {}) required_params = tool_function.get("parameters", {}).get("required", []) tool_method._parameter_schema = { @@ -71,17 +184,13 @@ async def tool_method(**kwargs): for param_name, param_details in param_props.items() } - # Register with server self.server.tool()(tool_method) logger.info(f"Registered tool: {tool_name}") def _build_docstring(self, tool_function: dict) -> str: - """Build a formatted docstring from tool function metadata.""" description = tool_function.get("description", "") param_props = tool_function.get("parameters", {}).get("properties", {}) required_params = tool_function.get("parameters", {}).get("required", []) - - # Build docstring (match original format) docstring = description if param_props: docstring += "\n\nParameters:\n" @@ -92,24 +201,17 @@ def _build_docstring(self, tool_function: dict) -> str: param_type = param_details.get("type", "any") param_desc = param_details.get("description", "") docstring += ( - f" {param_name} ({param_type}) {required_str}: {param_desc}\n" + f" {param_name} ({param_type}) {required_str}: {param_desc}\n" ) - return docstring def _build_signature(self, tool_function: dict) -> Signature: - """Build a function signature from tool function metadata.""" param_props = tool_function.get("parameters", {}).get("properties", {}) required_params = tool_function.get("parameters", {}).get("required", []) - parameters = [] - - # Follow original type mapping for param_name, param_details in param_props.items(): param_type = param_details.get("type", "") default = Parameter.empty if param_name in required_params else None - - # Map JSON Schema types to Python types (same as original) annotation = Any if param_type == "string": annotation = str @@ -123,8 +225,6 @@ def _build_signature(self, tool_function: dict) -> Signature: annotation = dict elif param_type == "array": annotation = list - - # Create parameter with same structure as original param = Parameter( name=param_name, kind=Parameter.KEYWORD_ONLY, @@ -132,49 +232,107 @@ def _build_signature(self, tool_function: dict) -> Signature: annotation=annotation, ) parameters.append(param) - return Signature(parameters=parameters) async def cleanup(self) -> None: - """Clean up server resources.""" - logger.info("Cleaning up resources") - # Follow original cleanup logic - only clean browser tool - if "browser" in self.tools and hasattr(self.tools["browser"], "cleanup"): - await self.tools["browser"].cleanup() + if hasattr(self, "_logger"): + self._logger.info("Cleaning up server resources") def register_all_tools(self) -> None: - """Register all tools with the server.""" + self._load_tools() for tool in self.tools.values(): self.register_tool(tool) - def run(self, transport: str = "stdio") -> None: - """Run the MCP server.""" - # Register all tools - self.register_all_tools() + def _build_sse_app(self) -> Starlette: + """ + Build a Starlette app wrapping FastMCP's SSE transport, + adding the health check route and Bearer auth middleware. + The /health route is registered FIRST so it responds immediately. + """ + # Get the raw Starlette app from FastMCP (has /sse and /messages/ routes) + fastmcp_app = self.server.sse_app() + + # Wrap it with our health check and auth middleware + app = Starlette( + debug=False, + routes=[ + Route("/health", endpoint=health_check), + Mount("/", app=fastmcp_app), + ], + middleware=[ + Middleware(BearerAuthMiddleware), + ], + ) + return app - # Register cleanup function (match original behavior) + def run(self, transport: str = "stdio") -> None: + """Run the MCP server in the specified transport mode.""" + global _server_ready atexit.register(lambda: asyncio.run(self.cleanup())) - # Start server (with same logging as original) - logger.info(f"Starting OpenManus server ({transport} mode)") - self.server.run(transport=transport) + if transport == "sse": + port = int(os.getenv("PORT", os.getenv("FASTMCP_PORT", "8000"))) + # Always bind to 0.0.0.0 in SSE mode so Railway's load balancer can reach us. + # FastMCP defaults to 127.0.0.1 which is unreachable from outside the container. + host = "0.0.0.0" + + # Build the Starlette app FIRST (fast - no heavy imports yet) + # so uvicorn can start serving /health immediately. + app = self._build_sse_app() + + # Register tools in a background task after the server is up. + # This ensures /health responds before the slow agent imports complete. + async def _startup(): + global _server_ready + import asyncio as _asyncio + + # Small delay to let uvicorn fully bind the port + await _asyncio.sleep(0.5) + logging.info("Loading tools and agents in background...") + # Debug: log the actual LLM config to verify env var substitution + try: + from app.config import config as _cfg + + _llm_configs = _cfg.llm + for _name, _llm in _llm_configs.items(): + logging.info( + f"[CONFIG] LLM[{_name}] base_url={_llm.base_url} model={_llm.model}" + ) + except Exception as _ce: + logging.warning(f"[CONFIG] Could not read LLM config: {_ce}") + self.register_all_tools() + _server_ready = True + logging.info( + f"OpenManus MCP server ready on {host}:{port}\n" + f" /health - health check\n" + f" /sse - MCP endpoint " + f"(auth: {'enabled' if os.getenv('MCP_SERVER_AUTH_TOKEN') else 'disabled'})\n" + f" Tools: {list(self.tools.keys())}" + ) + + # Add startup event to Starlette app + app.add_event_handler("startup", _startup) + + logging.info(f"Starting uvicorn on {host}:{port} ...") + uvicorn.run(app, host=host, port=port, log_level="info") + else: + # stdio mode - load everything synchronously (no healthcheck needed) + self.register_all_tools() + self.server.run(transport=transport) def parse_args() -> argparse.Namespace: - """Parse command line arguments.""" parser = argparse.ArgumentParser(description="OpenManus MCP Server") parser.add_argument( "--transport", - choices=["stdio"], + choices=["stdio", "sse"], default="stdio", - help="Communication method: stdio or http (default: stdio)", + help="Transport mode: stdio (local) or sse (web/Railway)", ) return parser.parse_args() if __name__ == "__main__": args = parse_args() - - # Create and run server (maintaining original flow) server = MCPServer() server.run(transport=args.transport) diff --git a/app/tool/chart_visualization/data_visualization.py b/app/tool/chart_visualization/data_visualization.py index 26dfaa985..de24b20a5 100644 --- a/app/tool/chart_visualization/data_visualization.py +++ b/app/tool/chart_visualization/data_visualization.py @@ -139,7 +139,7 @@ async def data_visualization( ) if len(error_list) > 0: return { - "observation": f"# Error chart generated{'\n'.join(error_list)}\n{self.success_output_template(success_list)}", + "observation": "# Error chart generated" + "\n".join(error_list) + "\n" + self.success_output_template(success_list), "success": False, } else: @@ -187,7 +187,7 @@ async def add_insighs( ) if len(error_list) > 0: return { - "observation": f"# Error in chart insights:{'\n'.join(error_list)}\n{success_template}", + "observation": "# Error in chart insights:" + "\n".join(error_list) + "\n" + success_template, "success": False, } else: diff --git a/config/config.toml b/config/config.toml new file mode 100644 index 000000000..d4b521422 --- /dev/null +++ b/config/config.toml @@ -0,0 +1,23 @@ +# OpenManus Production Configuration +# Secrets are injected via Railway environment variables using ${VAR_NAME} syntax. + +[llm] +model = "${LLM_MODEL}" +base_url = "${LLM_BASE_URL}" +api_key = "${LLM_API_KEY}" +max_tokens = 8192 +temperature = 0.0 + +[llm.vision] +model = "${LLM_MODEL}" +base_url = "${LLM_BASE_URL}" +api_key = "${LLM_API_KEY}" +max_tokens = 8192 +temperature = 0.0 + +[browser] +headless = true +disable_security = true + +[mcp] +server_reference = "app.mcp.server" diff --git a/daytona_stub/daytona/__init__.py b/daytona_stub/daytona/__init__.py new file mode 100644 index 000000000..f22753e9d --- /dev/null +++ b/daytona_stub/daytona/__init__.py @@ -0,0 +1,75 @@ +""" +Daytona SDK stub for environments where the real Daytona SDK is not available. + +This stub satisfies all import statements in app/daytona/ so that agents +(Manus, DataAnalysis, SWEAgent, BrowserAgent) can be imported and registered +as MCP tools. Actual Daytona sandbox operations will raise NotImplementedError +at runtime if called without a real Daytona API key and SDK. +""" +from enum import Enum +from typing import Any, Optional + + +class SandboxState(str, Enum): + RUNNING = "running" + STOPPED = "stopped" + ARCHIVED = "archived" + UNKNOWN = "unknown" + + +class _StubBase: + """Base for all stub classes — raises NotImplementedError on any method call.""" + + def __getattr__(self, name: str): + def _not_implemented(*args, **kwargs): + raise NotImplementedError( + f"Daytona SDK is not installed. " + f"Install the real 'daytona' package or provide a DAYTONA_API_KEY " + f"to use sandbox features. (Called: {self.__class__.__name__}.{name})" + ) + return _not_implemented + + +class DaytonaConfig(_StubBase): + def __init__(self, api_key: str = "", server_url: str = "", target: str = ""): + self.api_key = api_key + self.server_url = server_url + self.target = target + + +class Daytona(_StubBase): + def __init__(self, config: Optional[DaytonaConfig] = None): + self.config = config + + +class Sandbox(_StubBase): + pass + + +class Resources(_StubBase): + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + +class CreateSandboxFromImageParams(_StubBase): + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + +class SessionExecuteRequest(_StubBase): + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + +__all__ = [ + "Daytona", + "DaytonaConfig", + "Sandbox", + "SandboxState", + "Resources", + "CreateSandboxFromImageParams", + "SessionExecuteRequest", +] diff --git a/daytona_stub/setup.py b/daytona_stub/setup.py new file mode 100644 index 000000000..8c92a682c --- /dev/null +++ b/daytona_stub/setup.py @@ -0,0 +1,9 @@ +from setuptools import setup, find_packages + +setup( + name="daytona", + version="0.0.1", + description="Stub package for Daytona SDK — satisfies imports without real SDK", + packages=find_packages(), + python_requires=">=3.10", +) diff --git a/railway.json b/railway.json new file mode 100644 index 000000000..dbd25dc3e --- /dev/null +++ b/railway.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "DOCKERFILE", + "dockerfilePath": "Dockerfile" + }, + "deploy": { + "startCommand": "python run_mcp_server.py --transport sse", + "healthcheckPath": "/health", + "healthcheckTimeout": 120, + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 10 + } +} diff --git a/requirements.txt b/requirements.txt index aa7e6dc93..1707b3cd5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,9 +10,10 @@ tiktoken~=0.9.0 html2text~=2024.2.26 gymnasium~=1.1.1 -pillow~=11.1.0 +pillow>=10.4.0 browsergym~=0.13.3 uvicorn~=0.34.0 +structlog>=24.0.0 unidiff~=0.7.5 browser-use~=0.1.40 googlesearch-python~=1.3.0 @@ -36,7 +37,7 @@ boto3~=1.37.18 requests~=2.32.3 beautifulsoup4~=4.13.3 -crawl4ai~=0.6.3 +crawl4ai>=0.6.3 huggingface-hub~=0.29.2 setuptools~=75.8.0 diff --git a/run_mcp_server.py b/run_mcp_server.py index 00a86b305..015329279 100644 --- a/run_mcp_server.py +++ b/run_mcp_server.py @@ -1,5 +1,8 @@ # coding: utf-8 -# A shortcut to launch OpenManus MCP server, where its introduction also solves other import issues. +# Launch the OpenManus MCP server. +# Usage: +# stdio mode (local): python run_mcp_server.py +# SSE mode (Railway): python run_mcp_server.py --transport sse from app.mcp.server import MCPServer, parse_args