fleet-python 0.2.92__tar.gz → 0.2.94__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fleet_python-0.2.92/fleet_python.egg-info → fleet_python-0.2.94}/PKG-INFO +1 -1
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/__init__.py +1 -1
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/base.py +1 -1
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/agent/gemini_cua/Dockerfile +5 -4
- fleet_python-0.2.94/fleet/agent/gemini_cua/mcp/main.py +108 -0
- fleet_python-0.2.94/fleet/agent/gemini_cua/mcp_server/__init__.py +5 -0
- fleet_python-0.2.94/fleet/agent/gemini_cua/mcp_server/main.py +105 -0
- fleet_python-0.2.94/fleet/agent/gemini_cua/mcp_server/tools.py +178 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/agent/gemini_cua/requirements.txt +1 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/agent/gemini_cua/start.sh +2 -3
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/agent/orchestrator.py +57 -11
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/base.py +1 -1
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/cli.py +124 -22
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/models.py +1 -1
- {fleet_python-0.2.92 → fleet_python-0.2.94/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet_python.egg-info/SOURCES.txt +4 -2
- {fleet_python-0.2.92 → fleet_python-0.2.94}/pyproject.toml +1 -1
- fleet_python-0.2.92/fleet/agent/gemini_cua/mcp_server.py +0 -268
- fleet_python-0.2.92/fleet/agent/gemini_cua/playwright_utils.py +0 -440
- {fleet_python-0.2.92 → fleet_python-0.2.94}/LICENSE +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/README.md +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/diff_example.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example_account.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example_client.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example_sync.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example_task.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/openai_example.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/quickstart.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/resources/sqlite.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/agent/gemini_cua/agent.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/client.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/config.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/env/client.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/global_client.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/instance/client.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/resources/sqlite.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/tasks.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/types.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/utils/playwright.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/scripts/unasync.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/setup.cfg +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/tests/__init__.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.92 → fleet_python-0.2.94}/tests/test_verifier_from_string.py +0 -0
|
@@ -18,13 +18,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
18
18
|
|
|
19
19
|
WORKDIR /app
|
|
20
20
|
|
|
21
|
-
# Install Python deps
|
|
21
|
+
# Install Python deps (includes fleet-python for utils like fleet.utils.playwright)
|
|
22
22
|
COPY requirements.txt .
|
|
23
23
|
RUN pip install --no-cache-dir -r requirements.txt && playwright install chromium
|
|
24
24
|
|
|
25
|
-
# Copy server files (
|
|
26
|
-
COPY
|
|
27
|
-
|
|
25
|
+
# Copy MCP server files (standalone scripts that import from installed fleet-python)
|
|
26
|
+
COPY mcp_server/ ./mcp_server/
|
|
27
|
+
|
|
28
|
+
# Copy start script
|
|
28
29
|
COPY start.sh .
|
|
29
30
|
RUN chmod +x start.sh
|
|
30
31
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CUA Server - Computer Use Agent MCP Server
|
|
4
|
+
|
|
5
|
+
MCP server with playwright browser control using FastMCP's streamable-http transport.
|
|
6
|
+
|
|
7
|
+
Env vars:
|
|
8
|
+
FLEET_ENV_URL: URL to navigate to
|
|
9
|
+
PORT: Server port (default: 8765)
|
|
10
|
+
SCREEN_WIDTH/HEIGHT: Browser size
|
|
11
|
+
HEADLESS: "true" or "false" (default: true)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from mcp.server.fastmcp import FastMCP
|
|
20
|
+
from starlette.requests import Request
|
|
21
|
+
from starlette.responses import JSONResponse
|
|
22
|
+
|
|
23
|
+
from fleet.utils.playwright import PlaywrightComputer
|
|
24
|
+
|
|
25
|
+
# Support both module and standalone execution
|
|
26
|
+
try:
|
|
27
|
+
from .tools import register_tools
|
|
28
|
+
except ImportError:
|
|
29
|
+
from tools import register_tools
|
|
30
|
+
|
|
31
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# Setup
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
computer: Optional[PlaywrightComputer] = None
|
|
40
|
+
PORT = int(os.environ.get("PORT", "8765"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_computer() -> PlaywrightComputer:
|
|
44
|
+
"""Get the current computer instance."""
|
|
45
|
+
if computer is None:
|
|
46
|
+
raise RuntimeError("Computer not initialized")
|
|
47
|
+
return computer
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@asynccontextmanager
|
|
51
|
+
async def lifespan(app):
|
|
52
|
+
"""Initialize browser on startup, cleanup on shutdown."""
|
|
53
|
+
global computer
|
|
54
|
+
|
|
55
|
+
url = os.environ.get("FLEET_ENV_URL", "about:blank")
|
|
56
|
+
width = int(os.environ.get("SCREEN_WIDTH", "1366"))
|
|
57
|
+
height = int(os.environ.get("SCREEN_HEIGHT", "768"))
|
|
58
|
+
headless = os.environ.get("HEADLESS", "true").lower() == "true"
|
|
59
|
+
highlight = os.environ.get("HIGHLIGHT_MOUSE", "false").lower() == "true"
|
|
60
|
+
|
|
61
|
+
logger.info(f"CUA Server: {width}x{height}, headless={headless}, url={url}")
|
|
62
|
+
|
|
63
|
+
computer = PlaywrightComputer(
|
|
64
|
+
screen_size=(width, height),
|
|
65
|
+
initial_url=url,
|
|
66
|
+
headless=headless,
|
|
67
|
+
highlight_mouse=highlight or not headless,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
logger.info("Starting Playwright browser...")
|
|
72
|
+
await computer.start()
|
|
73
|
+
logger.info(f"Browser started, navigated to: {computer.current_url}")
|
|
74
|
+
yield
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Browser startup FAILED: {type(e).__name__}: {e}")
|
|
77
|
+
raise
|
|
78
|
+
finally:
|
|
79
|
+
logger.info("Stopping Playwright browser...")
|
|
80
|
+
try:
|
|
81
|
+
await computer.stop()
|
|
82
|
+
logger.info("Browser stopped")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Browser stop error: {type(e).__name__}: {e}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
mcp = FastMCP("cua-server", lifespan=lifespan, host="0.0.0.0", port=PORT)
|
|
88
|
+
|
|
89
|
+
# Register all tools
|
|
90
|
+
register_tools(mcp, get_computer)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# Routes
|
|
95
|
+
# =============================================================================
|
|
96
|
+
|
|
97
|
+
@mcp.custom_route("/health", methods=["GET"])
|
|
98
|
+
async def health_check(request: Request) -> JSONResponse:
|
|
99
|
+
return JSONResponse({"status": "ok", "url": computer.current_url if computer else ""})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# =============================================================================
|
|
103
|
+
# Main
|
|
104
|
+
# =============================================================================
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
logger.info(f"Starting CUA Server on port {PORT}")
|
|
108
|
+
mcp.run(transport="streamable-http")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CUA Server - Computer Use Agent MCP Server
|
|
4
|
+
|
|
5
|
+
MCP server with playwright browser control using FastMCP's streamable-http transport.
|
|
6
|
+
|
|
7
|
+
Env vars:
|
|
8
|
+
FLEET_ENV_URL: URL to navigate to
|
|
9
|
+
PORT: Server port (default: 8765)
|
|
10
|
+
SCREEN_WIDTH/HEIGHT: Browser size
|
|
11
|
+
HEADLESS: "true" or "false" (default: true)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from mcp.server.fastmcp import FastMCP
|
|
20
|
+
from starlette.requests import Request
|
|
21
|
+
from starlette.responses import JSONResponse
|
|
22
|
+
|
|
23
|
+
from fleet.utils.playwright import PlaywrightComputer
|
|
24
|
+
|
|
25
|
+
# Import tools (standalone execution in container)
|
|
26
|
+
from tools import register_tools
|
|
27
|
+
|
|
28
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# =============================================================================
|
|
33
|
+
# Setup
|
|
34
|
+
# =============================================================================
|
|
35
|
+
|
|
36
|
+
computer: Optional[PlaywrightComputer] = None
|
|
37
|
+
PORT = int(os.environ.get("PORT", "8765"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_computer() -> PlaywrightComputer:
|
|
41
|
+
"""Get the current computer instance."""
|
|
42
|
+
if computer is None:
|
|
43
|
+
raise RuntimeError("Computer not initialized")
|
|
44
|
+
return computer
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@asynccontextmanager
|
|
48
|
+
async def lifespan(app):
|
|
49
|
+
"""Initialize browser on startup, cleanup on shutdown."""
|
|
50
|
+
global computer
|
|
51
|
+
|
|
52
|
+
url = os.environ.get("FLEET_ENV_URL", "about:blank")
|
|
53
|
+
width = int(os.environ.get("SCREEN_WIDTH", "1366"))
|
|
54
|
+
height = int(os.environ.get("SCREEN_HEIGHT", "768"))
|
|
55
|
+
headless = os.environ.get("HEADLESS", "true").lower() == "true"
|
|
56
|
+
highlight = os.environ.get("HIGHLIGHT_MOUSE", "false").lower() == "true"
|
|
57
|
+
|
|
58
|
+
logger.info(f"CUA Server: {width}x{height}, headless={headless}, url={url}")
|
|
59
|
+
|
|
60
|
+
computer = PlaywrightComputer(
|
|
61
|
+
screen_size=(width, height),
|
|
62
|
+
initial_url=url,
|
|
63
|
+
headless=headless,
|
|
64
|
+
highlight_mouse=highlight or not headless,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
logger.info("Starting Playwright browser...")
|
|
69
|
+
await computer.start()
|
|
70
|
+
logger.info(f"Browser started, navigated to: {computer.current_url}")
|
|
71
|
+
yield
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Browser startup FAILED: {type(e).__name__}: {e}")
|
|
74
|
+
raise
|
|
75
|
+
finally:
|
|
76
|
+
logger.info("Stopping Playwright browser...")
|
|
77
|
+
try:
|
|
78
|
+
await computer.stop()
|
|
79
|
+
logger.info("Browser stopped")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Browser stop error: {type(e).__name__}: {e}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
mcp = FastMCP("cua-server", lifespan=lifespan, host="0.0.0.0", port=PORT)
|
|
85
|
+
|
|
86
|
+
# Register all tools
|
|
87
|
+
register_tools(mcp, get_computer)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# =============================================================================
|
|
91
|
+
# Routes
|
|
92
|
+
# =============================================================================
|
|
93
|
+
|
|
94
|
+
@mcp.custom_route("/health", methods=["GET"])
|
|
95
|
+
async def health_check(request: Request) -> JSONResponse:
|
|
96
|
+
return JSONResponse({"status": "ok", "url": computer.current_url if computer else ""})
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# =============================================================================
|
|
100
|
+
# Main
|
|
101
|
+
# =============================================================================
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
logger.info(f"Starting CUA Server on port {PORT}")
|
|
105
|
+
mcp.run(transport="streamable-http")
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""MCP tool definitions for CUA server."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
8
|
+
from mcp.types import ImageContent, TextContent
|
|
9
|
+
|
|
10
|
+
from fleet.utils.playwright import PlaywrightComputer, KEY_SPEC
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_tools(mcp: FastMCP, get_computer: Callable[[], PlaywrightComputer]) -> None:
|
|
16
|
+
"""Register all CUA tools with the MCP server.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
mcp: FastMCP server instance
|
|
20
|
+
get_computer: Callable that returns the current PlaywrightComputer instance
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def _dx(x: int) -> int:
|
|
24
|
+
"""Denormalize x: [0,1000] -> pixels."""
|
|
25
|
+
return int(x / 1000 * get_computer().width)
|
|
26
|
+
|
|
27
|
+
def _dy(y: int) -> int:
|
|
28
|
+
"""Denormalize y: [0,1000] -> pixels."""
|
|
29
|
+
return int(y / 1000 * get_computer().height)
|
|
30
|
+
|
|
31
|
+
def _screenshot_response(img: bytes) -> list:
|
|
32
|
+
"""Return screenshot as proper MCP content types."""
|
|
33
|
+
computer = get_computer()
|
|
34
|
+
return [
|
|
35
|
+
ImageContent(type="image", data=base64.b64encode(img).decode(), mimeType="image/png"),
|
|
36
|
+
TextContent(type="text", text=f"URL: {computer.current_url}"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
@mcp.tool()
|
|
40
|
+
async def computer_screenshot() -> list:
|
|
41
|
+
"""Takes a screenshot of the computer screen. Use this to see what's on screen."""
|
|
42
|
+
logger.info("computer_screenshot()")
|
|
43
|
+
try:
|
|
44
|
+
result = await get_computer().screenshot()
|
|
45
|
+
logger.info(f"computer_screenshot() -> {len(result)} bytes")
|
|
46
|
+
return _screenshot_response(result)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"computer_screenshot() FAILED: {type(e).__name__}: {e}")
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
@mcp.tool()
|
|
52
|
+
async def mouse_click(x: int, y: int, button: str, repeats: int = 1) -> None:
|
|
53
|
+
"""Performs a mouse click.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
x: The normalized x coordinate within the [0, 1000] range of the image.
|
|
57
|
+
y: The normalized y coordinate within the [0, 1000] range of the image.
|
|
58
|
+
button: The button to click. Either 'left', 'middle' or 'right'.
|
|
59
|
+
repeats: The number of times to click. Default is 1.
|
|
60
|
+
"""
|
|
61
|
+
logger.info(f"mouse_click({x}, {y}, {button}, {repeats})")
|
|
62
|
+
try:
|
|
63
|
+
await get_computer().mouse_click(_dx(x), _dy(y), button, repeats)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"mouse_click FAILED: {type(e).__name__}: {e}")
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
async def mouse_move(x: int, y: int) -> None:
|
|
70
|
+
"""Moves the mouse to a new position.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
x: The normalized x coordinate within the [0, 1000] range of the image.
|
|
74
|
+
y: The normalized y coordinate within the [0, 1000] range of the image.
|
|
75
|
+
"""
|
|
76
|
+
logger.info(f"mouse_move({x}, {y})")
|
|
77
|
+
await get_computer().mouse_move(_dx(x), _dy(y))
|
|
78
|
+
|
|
79
|
+
@mcp.tool()
|
|
80
|
+
async def mouse_down(button: str) -> None:
|
|
81
|
+
"""Keeps a mouse button down.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
button: The button to press down. Either 'left', 'middle' or 'right'.
|
|
85
|
+
"""
|
|
86
|
+
logger.info(f"mouse_down({button})")
|
|
87
|
+
await get_computer().mouse_down(button)
|
|
88
|
+
|
|
89
|
+
@mcp.tool()
|
|
90
|
+
async def mouse_up(button: str) -> None:
|
|
91
|
+
"""Releases a mouse button after executing a mouse down action.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
button: The button to release. Either 'left', 'middle' or 'right'.
|
|
95
|
+
"""
|
|
96
|
+
logger.info(f"mouse_up({button})")
|
|
97
|
+
await get_computer().mouse_up(button)
|
|
98
|
+
|
|
99
|
+
@mcp.tool()
|
|
100
|
+
async def mouse_scroll(dx: int, dy: int) -> None:
|
|
101
|
+
"""Uses the mouse to perform a two dimensional scroll.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
dx: The number of pixels to scroll horizontally.
|
|
105
|
+
dy: The number of pixels to scroll vertically.
|
|
106
|
+
"""
|
|
107
|
+
logger.info(f"mouse_scroll({dx}, {dy})")
|
|
108
|
+
await get_computer().mouse_scroll(dx, dy)
|
|
109
|
+
|
|
110
|
+
@mcp.tool()
|
|
111
|
+
async def mouse_drag(x_start: int, y_start: int, x_end: int, y_end: int, button: str = "left") -> None:
|
|
112
|
+
"""Drag mouse from a point A to a point B.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
x_start: The x coordinate of the starting point normalized within [0, 1000].
|
|
116
|
+
y_start: The y coordinate of the starting point normalized within [0, 1000].
|
|
117
|
+
x_end: The x coordinate of the destination point normalized within [0, 1000].
|
|
118
|
+
y_end: The y coordinate of the destination point normalized within [0, 1000].
|
|
119
|
+
button: The mouse button: left, right, middle. Default is 'left'.
|
|
120
|
+
"""
|
|
121
|
+
logger.info(f"mouse_drag({x_start}, {y_start} -> {x_end}, {y_end})")
|
|
122
|
+
await get_computer().mouse_drag(_dx(x_start), _dy(y_start), _dx(x_end), _dy(y_end), button)
|
|
123
|
+
|
|
124
|
+
@mcp.tool()
|
|
125
|
+
async def wait(seconds: int) -> None:
|
|
126
|
+
"""Waits for a given number of seconds. Use if the screen is blank or page is loading.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
seconds: The number of seconds to wait.
|
|
130
|
+
"""
|
|
131
|
+
logger.info(f"wait({seconds})")
|
|
132
|
+
await get_computer().wait(seconds)
|
|
133
|
+
|
|
134
|
+
@mcp.tool()
|
|
135
|
+
async def type_text(input_text: str, press_enter: bool) -> None:
|
|
136
|
+
"""Type text on a keyboard.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
input_text: The input text to type.
|
|
140
|
+
press_enter: Whether to press enter after typing.
|
|
141
|
+
"""
|
|
142
|
+
logger.info(f"type_text({input_text[:50]}{'...' if len(input_text) > 50 else ''}, enter={press_enter})")
|
|
143
|
+
try:
|
|
144
|
+
await get_computer().type_text(input_text, press_enter)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.error(f"type_text FAILED: {type(e).__name__}: {e}")
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
@mcp.tool()
|
|
150
|
+
async def key_combination(keys_to_press: list[str]) -> None:
|
|
151
|
+
f"""Performs a key combination. {KEY_SPEC}
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
keys_to_press: The list of keys to press.
|
|
155
|
+
"""
|
|
156
|
+
logger.info(f"key_combination({keys_to_press})")
|
|
157
|
+
await get_computer().key_combination(keys_to_press)
|
|
158
|
+
|
|
159
|
+
@mcp.tool()
|
|
160
|
+
async def key_down(key: str) -> None:
|
|
161
|
+
f"""Keeps a keyboard key down. {KEY_SPEC}
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
key: The key to press down.
|
|
165
|
+
"""
|
|
166
|
+
logger.info(f"key_down({key})")
|
|
167
|
+
await get_computer().key_down(key)
|
|
168
|
+
|
|
169
|
+
@mcp.tool()
|
|
170
|
+
async def key_up(key: str) -> None:
|
|
171
|
+
f"""Releases a keyboard key after executing a key down action. {KEY_SPEC}
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
key: The key to press up.
|
|
175
|
+
"""
|
|
176
|
+
logger.info(f"key_up({key})")
|
|
177
|
+
await get_computer().key_up(key)
|
|
178
|
+
|
|
@@ -236,13 +236,18 @@ class AgentOrchestrator:
|
|
|
236
236
|
from fleet._async import load_tasks
|
|
237
237
|
from rich.console import Console
|
|
238
238
|
from rich.live import Live
|
|
239
|
+
from rich.panel import Panel
|
|
239
240
|
from rich.spinner import Spinner
|
|
240
241
|
|
|
241
242
|
console = Console()
|
|
242
243
|
|
|
243
244
|
# Create job via Fleet API (name generated server-side)
|
|
244
245
|
self._job_id = await fleet.job_async()
|
|
245
|
-
console.print(
|
|
246
|
+
console.print(Panel(
|
|
247
|
+
f"[bold]Live agent traces[/bold]\n\n https://www.fleetai.com/dashboard/jobs/{self._job_id}",
|
|
248
|
+
border_style="cyan",
|
|
249
|
+
))
|
|
250
|
+
console.print()
|
|
246
251
|
|
|
247
252
|
# Create log directory: ~/.fleet/logs/{job_id}/
|
|
248
253
|
self._log_dir = Path.home() / ".fleet" / "logs" / self._job_id
|
|
@@ -278,6 +283,9 @@ class AgentOrchestrator:
|
|
|
278
283
|
|
|
279
284
|
semaphore = asyncio.Semaphore(self.config.max_concurrent)
|
|
280
285
|
results = [None] * len(tasks)
|
|
286
|
+
completed_count = 0
|
|
287
|
+
passed_count = 0
|
|
288
|
+
total_count = len(tasks)
|
|
281
289
|
|
|
282
290
|
with Progress(
|
|
283
291
|
SpinnerColumn(),
|
|
@@ -286,12 +294,23 @@ class AgentOrchestrator:
|
|
|
286
294
|
TaskProgressColumn(),
|
|
287
295
|
console=console,
|
|
288
296
|
) as progress:
|
|
289
|
-
task_progress = progress.add_task(
|
|
297
|
+
task_progress = progress.add_task(
|
|
298
|
+
f"[cyan]Running ({completed_count}/{total_count}) | {passed_count} passed[/cyan]",
|
|
299
|
+
total=len(tasks)
|
|
300
|
+
)
|
|
290
301
|
|
|
291
302
|
async def run_with_semaphore(idx, task):
|
|
303
|
+
nonlocal completed_count, passed_count
|
|
292
304
|
async with semaphore:
|
|
293
305
|
result = await self._run_task(task)
|
|
294
|
-
|
|
306
|
+
completed_count += 1
|
|
307
|
+
if result.verification_success:
|
|
308
|
+
passed_count += 1
|
|
309
|
+
progress.update(
|
|
310
|
+
task_progress,
|
|
311
|
+
advance=1,
|
|
312
|
+
description=f"[cyan]Running ({completed_count}/{total_count}) | {passed_count} passed[/cyan]"
|
|
313
|
+
)
|
|
295
314
|
return idx, result
|
|
296
315
|
|
|
297
316
|
completed = await asyncio.gather(
|
|
@@ -329,7 +348,7 @@ class AgentOrchestrator:
|
|
|
329
348
|
# Print summary statistics
|
|
330
349
|
self._print_stats()
|
|
331
350
|
|
|
332
|
-
return final
|
|
351
|
+
return final, self._job_id
|
|
333
352
|
|
|
334
353
|
async def _build_docker_image(self, agent_path: Path):
|
|
335
354
|
"""Build Docker image for CUA server."""
|
|
@@ -670,6 +689,7 @@ class AgentOrchestrator:
|
|
|
670
689
|
|
|
671
690
|
env.update(
|
|
672
691
|
{
|
|
692
|
+
"PYTHONUNBUFFERED": "1", # Ensure real-time output
|
|
673
693
|
"FLEET_MCP_URL": f"http://localhost:{port}",
|
|
674
694
|
"FLEET_SESSION_LOG": str(
|
|
675
695
|
session_log_file
|
|
@@ -695,9 +715,36 @@ class AgentOrchestrator:
|
|
|
695
715
|
env=env,
|
|
696
716
|
)
|
|
697
717
|
|
|
718
|
+
short_key = task_key[:20]
|
|
719
|
+
stdout_lines = []
|
|
720
|
+
stderr_lines = []
|
|
721
|
+
|
|
722
|
+
async def read_stdout():
|
|
723
|
+
while True:
|
|
724
|
+
line = await proc.stdout.readline()
|
|
725
|
+
if not line:
|
|
726
|
+
break
|
|
727
|
+
line_str = line.decode().rstrip()
|
|
728
|
+
stdout_lines.append(line_str)
|
|
729
|
+
# Show step updates in real-time
|
|
730
|
+
if line_str.startswith("STEP:") or line_str.startswith("Step "):
|
|
731
|
+
print(f"[{short_key}] {line_str}")
|
|
732
|
+
elif self.config.verbose:
|
|
733
|
+
logger.info(f"[{short_key}] {line_str}")
|
|
734
|
+
|
|
735
|
+
async def read_stderr():
|
|
736
|
+
while True:
|
|
737
|
+
line = await proc.stderr.readline()
|
|
738
|
+
if not line:
|
|
739
|
+
break
|
|
740
|
+
line_str = line.decode().rstrip()
|
|
741
|
+
stderr_lines.append(line_str)
|
|
742
|
+
if self.config.verbose:
|
|
743
|
+
logger.warning(f"[{short_key}] stderr: {line_str}")
|
|
744
|
+
|
|
698
745
|
try:
|
|
699
|
-
|
|
700
|
-
proc.
|
|
746
|
+
await asyncio.wait_for(
|
|
747
|
+
asyncio.gather(read_stdout(), read_stderr(), proc.wait()),
|
|
701
748
|
timeout=self.config.timeout_seconds,
|
|
702
749
|
)
|
|
703
750
|
except asyncio.TimeoutError:
|
|
@@ -710,8 +757,8 @@ class AgentOrchestrator:
|
|
|
710
757
|
)
|
|
711
758
|
|
|
712
759
|
# Parse result from stdout/stderr
|
|
713
|
-
stdout_str =
|
|
714
|
-
stderr_str =
|
|
760
|
+
stdout_str = "\n".join(stdout_lines)
|
|
761
|
+
stderr_str = "\n".join(stderr_lines)
|
|
715
762
|
|
|
716
763
|
# Show full output in verbose mode
|
|
717
764
|
if self.config.verbose:
|
|
@@ -725,7 +772,6 @@ class AgentOrchestrator:
|
|
|
725
772
|
|
|
726
773
|
# Always show stderr if agent crashed (non-zero exit or has stderr)
|
|
727
774
|
if proc.returncode != 0 or stderr_str:
|
|
728
|
-
short_key = task_key[:20]
|
|
729
775
|
if stderr_str:
|
|
730
776
|
print(f"[{short_key}] Agent stderr: {stderr_str[:500]}")
|
|
731
777
|
|
|
@@ -773,7 +819,7 @@ async def run_agent(
|
|
|
773
819
|
api_keys: Optional[Dict[str, str]] = None,
|
|
774
820
|
headful: bool = False,
|
|
775
821
|
verbose: bool = False,
|
|
776
|
-
) -> List[TaskResult]:
|
|
822
|
+
) -> Tuple[List[TaskResult], str]:
|
|
777
823
|
"""Run agent on Fleet tasks.
|
|
778
824
|
|
|
779
825
|
Args:
|
|
@@ -789,7 +835,7 @@ async def run_agent(
|
|
|
789
835
|
verbose: Enable verbose agent logging
|
|
790
836
|
|
|
791
837
|
Returns:
|
|
792
|
-
List of TaskResult
|
|
838
|
+
Tuple of (List of TaskResult, job_id)
|
|
793
839
|
"""
|
|
794
840
|
config = AgentConfig(
|
|
795
841
|
project_key=project_key,
|