fleet-python 0.2.93__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.
Files changed (118) hide show
  1. {fleet_python-0.2.93/fleet_python.egg-info → fleet_python-0.2.94}/PKG-INFO +1 -1
  2. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/agent/gemini_cua/Dockerfile +5 -4
  3. fleet_python-0.2.94/fleet/agent/gemini_cua/mcp/main.py +108 -0
  4. fleet_python-0.2.94/fleet/agent/gemini_cua/mcp_server/__init__.py +5 -0
  5. fleet_python-0.2.94/fleet/agent/gemini_cua/mcp_server/main.py +105 -0
  6. fleet_python-0.2.94/fleet/agent/gemini_cua/mcp_server/tools.py +178 -0
  7. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/agent/gemini_cua/requirements.txt +1 -0
  8. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/agent/gemini_cua/start.sh +2 -3
  9. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/agent/orchestrator.py +54 -8
  10. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/cli.py +48 -20
  11. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/models.py +1 -1
  12. {fleet_python-0.2.93 → fleet_python-0.2.94/fleet_python.egg-info}/PKG-INFO +1 -1
  13. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet_python.egg-info/SOURCES.txt +4 -2
  14. {fleet_python-0.2.93 → fleet_python-0.2.94}/pyproject.toml +1 -1
  15. fleet_python-0.2.93/fleet/agent/gemini_cua/mcp_server.py +0 -268
  16. fleet_python-0.2.93/fleet/agent/gemini_cua/playwright_utils.py +0 -440
  17. {fleet_python-0.2.93 → fleet_python-0.2.94}/LICENSE +0 -0
  18. {fleet_python-0.2.93 → fleet_python-0.2.94}/README.md +0 -0
  19. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/diff_example.py +0 -0
  20. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/dsl_example.py +0 -0
  21. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example.py +0 -0
  22. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/exampleResume.py +0 -0
  23. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example_account.py +0 -0
  24. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example_action_log.py +0 -0
  25. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example_client.py +0 -0
  26. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example_mcp_anthropic.py +0 -0
  27. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example_mcp_openai.py +0 -0
  28. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example_sync.py +0 -0
  29. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example_task.py +0 -0
  30. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example_tasks.py +0 -0
  31. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/example_verifier.py +0 -0
  32. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/export_tasks.py +0 -0
  33. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/fetch_tasks.py +0 -0
  34. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/gemini_example.py +0 -0
  35. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/import_tasks.py +0 -0
  36. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/iterate_verifiers.py +0 -0
  37. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/json_tasks_example.py +0 -0
  38. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/nova_act_example.py +0 -0
  39. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/openai_example.py +0 -0
  40. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/openai_simple_example.py +0 -0
  41. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/query_builder_example.py +0 -0
  42. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/quickstart.py +0 -0
  43. {fleet_python-0.2.93 → fleet_python-0.2.94}/examples/test_cdp_logging.py +0 -0
  44. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/__init__.py +0 -0
  45. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/__init__.py +0 -0
  46. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/base.py +0 -0
  47. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/client.py +0 -0
  48. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/env/__init__.py +0 -0
  49. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/env/client.py +0 -0
  50. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/exceptions.py +0 -0
  51. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/global_client.py +0 -0
  52. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/instance/__init__.py +0 -0
  53. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/instance/base.py +0 -0
  54. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/instance/client.py +0 -0
  55. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/models.py +0 -0
  56. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/resources/__init__.py +0 -0
  57. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/resources/base.py +0 -0
  58. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/resources/browser.py +0 -0
  59. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/resources/mcp.py +0 -0
  60. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/resources/sqlite.py +0 -0
  61. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/tasks.py +0 -0
  62. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/verifiers/__init__.py +0 -0
  63. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/verifiers/bundler.py +0 -0
  64. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/_async/verifiers/verifier.py +0 -0
  65. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/agent/__init__.py +0 -0
  66. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/agent/gemini_cua/__init__.py +0 -0
  67. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/agent/gemini_cua/agent.py +0 -0
  68. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/agent/types.py +0 -0
  69. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/agent/utils.py +0 -0
  70. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/base.py +0 -0
  71. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/client.py +0 -0
  72. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/config.py +0 -0
  73. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/env/__init__.py +0 -0
  74. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/env/client.py +0 -0
  75. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/eval/__init__.py +0 -0
  76. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/eval/uploader.py +0 -0
  77. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/exceptions.py +0 -0
  78. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/global_client.py +0 -0
  79. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/instance/__init__.py +0 -0
  80. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/instance/base.py +0 -0
  81. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/instance/client.py +0 -0
  82. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/instance/models.py +0 -0
  83. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/proxy/__init__.py +0 -0
  84. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/proxy/proxy.py +0 -0
  85. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/proxy/whitelist.py +0 -0
  86. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/resources/__init__.py +0 -0
  87. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/resources/base.py +0 -0
  88. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/resources/browser.py +0 -0
  89. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/resources/mcp.py +0 -0
  90. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/resources/sqlite.py +0 -0
  91. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/tasks.py +0 -0
  92. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/types.py +0 -0
  93. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/utils/__init__.py +0 -0
  94. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/utils/http_logging.py +0 -0
  95. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/utils/logging.py +0 -0
  96. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/utils/playwright.py +0 -0
  97. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/verifiers/__init__.py +0 -0
  98. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/verifiers/bundler.py +0 -0
  99. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/verifiers/code.py +0 -0
  100. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/verifiers/db.py +0 -0
  101. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/verifiers/decorator.py +0 -0
  102. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/verifiers/parse.py +0 -0
  103. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/verifiers/sql_differ.py +0 -0
  104. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet/verifiers/verifier.py +0 -0
  105. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet_python.egg-info/dependency_links.txt +0 -0
  106. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet_python.egg-info/entry_points.txt +0 -0
  107. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet_python.egg-info/requires.txt +0 -0
  108. {fleet_python-0.2.93 → fleet_python-0.2.94}/fleet_python.egg-info/top_level.txt +0 -0
  109. {fleet_python-0.2.93 → fleet_python-0.2.94}/scripts/fix_sync_imports.py +0 -0
  110. {fleet_python-0.2.93 → fleet_python-0.2.94}/scripts/unasync.py +0 -0
  111. {fleet_python-0.2.93 → fleet_python-0.2.94}/setup.cfg +0 -0
  112. {fleet_python-0.2.93 → fleet_python-0.2.94}/tests/__init__.py +0 -0
  113. {fleet_python-0.2.93 → fleet_python-0.2.94}/tests/test_app_method.py +0 -0
  114. {fleet_python-0.2.93 → fleet_python-0.2.94}/tests/test_expect_only.py +0 -0
  115. {fleet_python-0.2.93 → fleet_python-0.2.94}/tests/test_instance_dispatch.py +0 -0
  116. {fleet_python-0.2.93 → fleet_python-0.2.94}/tests/test_sqlite_resource_dual_mode.py +0 -0
  117. {fleet_python-0.2.93 → fleet_python-0.2.94}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  118. {fleet_python-0.2.93 → fleet_python-0.2.94}/tests/test_verifier_from_string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.93
3
+ Version: 0.2.94
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.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 (all from same directory)
26
- COPY playwright_utils.py .
27
- COPY mcp_server.py .
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,5 @@
1
+ """MCP server for Gemini CUA agent.
2
+
3
+ This folder is named 'mcp_server' instead of 'mcp' to avoid shadowing the 'mcp' package.
4
+ """
5
+
@@ -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
+
@@ -1,3 +1,4 @@
1
+ fleet-python
1
2
  playwright>=1.40.0
2
3
  mcp[cli]>=1.2.0
3
4
  uvicorn>=0.30.0
@@ -26,6 +26,5 @@ if [ "$HEADLESS" != "true" ]; then
26
26
  echo ""
27
27
  fi
28
28
 
29
- # Start the MCP server
30
- exec python mcp_server.py
31
-
29
+ # Start the MCP server (standalone script, imports from installed fleet-python)
30
+ exec python mcp_server/main.py
@@ -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(f"Job: https://fleetai.com/dashboard/jobs/{self._job_id}")
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("Running tasks", total=len(tasks))
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
- progress.update(task_progress, advance=1)
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(
@@ -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
- stdout, stderr = await asyncio.wait_for(
700
- proc.communicate(),
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 = stdout.decode()
714
- stderr_str = stderr.decode()
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