@iaforged/context-code 2.3.3 → 2.3.5
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.
- package/context-bootstrap.js +7 -5
- package/dist/src/commands/login/login.js +1 -1
- package/dist/src/components/BaseTextInput.js +1 -1
- package/dist/src/components/LogoV2/AnimatedClawd.js +1 -1
- package/dist/src/components/LogoV2/Clawd.js +1 -1
- package/dist/src/components/LogoV2/LogoV2.js +1 -1
- package/dist/src/components/LogoV2/WelcomeV2.js +1 -1
- package/dist/src/components/PromptInput/PromptInputFooterLeftSide.js +1 -1
- package/dist/src/components/SessionTokenFooter.js +1 -1
- package/dist/src/components/Spinner.js +1 -1
- package/dist/src/components/Stats.js +1 -1
- package/dist/src/components/TeleportProgress.js +1 -1
- package/dist/src/components/TextInput.js +1 -1
- package/dist/src/components/design-system/ThemeProvider.js +1 -1
- package/dist/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.js +1 -1
- package/dist/src/main.js +1 -1
- package/dist/src/query/stopHooks.js +1 -1
- package/dist/src/screens/REPL.js +1 -1
- package/dist/src/services/PromptSuggestion/promptSuggestion.js +1 -1
- package/dist/src/services/analytics/config.js +1 -1
- package/dist/src/services/analytics/datadog.js +1 -1
- package/dist/src/services/mcp/config.js +1 -1
- package/dist/src/services/tips/tipRegistry.js +1 -1
- package/dist/src/services/toolUseSummary/toolUseSummaryGenerator.js +1 -1
- package/dist/src/tools/BriefTool/UI.js +1 -1
- package/dist/src/utils/computerControlMcp/mcpServer.js +1 -1
- package/dist/src/utils/computerControlMcp/server/.gitattributes +18 -0
- package/dist/src/utils/computerControlMcp/server/Dockerfile +25 -0
- package/dist/src/utils/computerControlMcp/server/LICENSE +21 -0
- package/dist/src/utils/computerControlMcp/server/MANIFEST.in +10 -0
- package/dist/src/utils/computerControlMcp/server/README.md +193 -0
- package/dist/src/utils/computerControlMcp/server/demonstration.gif +0 -0
- package/dist/src/utils/computerControlMcp/server/icon.png +0 -0
- package/dist/src/utils/computerControlMcp/server/pyproject.toml +52 -0
- package/dist/src/utils/computerControlMcp/server/smithery.yaml +13 -0
- package/dist/src/utils/computerControlMcp/server/src/README.md +12 -0
- package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/FZYTK.TTF +0 -0
- package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/__init__.py +11 -0
- package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/__main__.py +21 -0
- package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/cli.py +128 -0
- package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/core.py +1008 -0
- package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/gui.py +126 -0
- package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/server.py +15 -0
- package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/test.py +346 -0
- package/dist/src/utils/computerControlMcp/server/src/computer_control_mcp/test_image.png +0 -0
- package/dist/src/utils/computerControlMcp/server/tests/README.md +22 -0
- package/dist/src/utils/computerControlMcp/server/tests/conftest.py +10 -0
- package/dist/src/utils/computerControlMcp/server/tests/rapidocr_test.py +21 -0
- package/dist/src/utils/computerControlMcp/server/tests/run_cli.py +9 -0
- package/dist/src/utils/computerControlMcp/server/tests/run_server.py +15 -0
- package/dist/src/utils/computerControlMcp/server/tests/setup.py +16 -0
- package/dist/src/utils/computerControlMcp/server/tests/test_computer_control.py +161 -0
- package/dist/src/utils/computerControlMcp/server/tests/test_screenshot.py +14 -0
- package/dist/src/utils/computerControlMcp/server/tests/test_wgc_env_var.py +42 -0
- package/dist/src/utils/computerControlMcp/server/tests/test_wgc_screenshot.py +67 -0
- package/dist/src/utils/computerControlMcp/server/uv.lock +4986 -0
- package/dist/src/utils/computerControlMcp/setup.js +1 -1
- package/dist/src/utils/logoV2Utils.js +1 -1
- package/dist/src/utils/model/configs.js +1 -1
- package/dist/src/utils/model/model.js +1 -1
- package/dist/src/utils/model/modelOptions.js +1 -1
- package/dist/src/utils/model/providerModels.js +1 -1
- package/dist/src/utils/sembleMcp/setup.js +1 -1
- package/dist/src/utils/theme.js +1 -1
- package/dist/src/utils/themes/bootstrap.js +1 -1
- package/dist/src/utils/themes/opencodeMapper.js +1 -1
- package/dist/webapp/chunk-VAB2VXFI.js +1 -1
- package/dist/webapp/ngsw.json +1 -1
- package/dist/webapp/polyfills-7R4CRVNH.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GUI Test Harness for Computer Control MCP.
|
|
3
|
+
|
|
4
|
+
This module provides a graphical user interface for testing the Computer Control MCP functionality.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import tkinter as tk
|
|
8
|
+
from tkinter import ttk, scrolledtext
|
|
9
|
+
from PIL import Image, ImageTk
|
|
10
|
+
import pyautogui
|
|
11
|
+
import json
|
|
12
|
+
import io
|
|
13
|
+
|
|
14
|
+
from computer_control_mcp.core import mcp
|
|
15
|
+
|
|
16
|
+
class TestHarnessGUI:
|
|
17
|
+
def __init__(self, root):
|
|
18
|
+
self.root = root
|
|
19
|
+
self.root.title("Computer Control Test Harness")
|
|
20
|
+
self.root.geometry("800x600")
|
|
21
|
+
|
|
22
|
+
# Create main frame with scrollbar
|
|
23
|
+
self.main_frame = ttk.Frame(root)
|
|
24
|
+
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
25
|
+
|
|
26
|
+
# Create test sections
|
|
27
|
+
self.create_click_test_section()
|
|
28
|
+
self.create_type_text_section()
|
|
29
|
+
self.create_screenshot_section()
|
|
30
|
+
self.create_output_section()
|
|
31
|
+
|
|
32
|
+
# Initialize test results
|
|
33
|
+
self.test_results = {}
|
|
34
|
+
|
|
35
|
+
def create_click_test_section(self):
|
|
36
|
+
frame = ttk.LabelFrame(self.main_frame, text="Mouse Click Test")
|
|
37
|
+
frame.pack(fill=tk.X, padx=5, pady=5)
|
|
38
|
+
|
|
39
|
+
# Coordinates input
|
|
40
|
+
coord_frame = ttk.Frame(frame)
|
|
41
|
+
coord_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
42
|
+
|
|
43
|
+
ttk.Label(coord_frame, text="X:").pack(side=tk.LEFT)
|
|
44
|
+
self.x_entry = ttk.Entry(coord_frame, width=10)
|
|
45
|
+
self.x_entry.pack(side=tk.LEFT, padx=5)
|
|
46
|
+
|
|
47
|
+
ttk.Label(coord_frame, text="Y:").pack(side=tk.LEFT)
|
|
48
|
+
self.y_entry = ttk.Entry(coord_frame, width=10)
|
|
49
|
+
self.y_entry.pack(side=tk.LEFT, padx=5)
|
|
50
|
+
|
|
51
|
+
ttk.Button(frame, text="Test Click", command=self.test_click).pack(pady=5)
|
|
52
|
+
|
|
53
|
+
def create_type_text_section(self):
|
|
54
|
+
frame = ttk.LabelFrame(self.main_frame, text="Type Text Test")
|
|
55
|
+
frame.pack(fill=tk.X, padx=5, pady=5)
|
|
56
|
+
|
|
57
|
+
ttk.Label(frame, text="Text to type:").pack(pady=2)
|
|
58
|
+
self.text_entry = ttk.Entry(frame)
|
|
59
|
+
self.text_entry.pack(fill=tk.X, padx=5, pady=2)
|
|
60
|
+
|
|
61
|
+
ttk.Button(frame, text="Test Type Text", command=self.test_type_text).pack(pady=5)
|
|
62
|
+
|
|
63
|
+
def create_screenshot_section(self):
|
|
64
|
+
frame = ttk.LabelFrame(self.main_frame, text="Screenshot Test")
|
|
65
|
+
frame.pack(fill=tk.X, padx=5, pady=5)
|
|
66
|
+
|
|
67
|
+
ttk.Button(frame, text="Take Screenshot", command=self.test_screenshot).pack(pady=5)
|
|
68
|
+
|
|
69
|
+
# Canvas for screenshot preview
|
|
70
|
+
self.screenshot_canvas = tk.Canvas(frame, width=200, height=150)
|
|
71
|
+
self.screenshot_canvas.pack(pady=5)
|
|
72
|
+
|
|
73
|
+
def create_output_section(self):
|
|
74
|
+
frame = ttk.LabelFrame(self.main_frame, text="Test Output")
|
|
75
|
+
frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
76
|
+
|
|
77
|
+
self.output_text = scrolledtext.ScrolledText(frame, height=10)
|
|
78
|
+
self.output_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
79
|
+
|
|
80
|
+
def log_output(self, test_name, request_data, response_data):
|
|
81
|
+
self.output_text.insert(tk.END, f"\n===== TEST: {test_name} =====\n")
|
|
82
|
+
self.output_text.insert(tk.END, f"REQUEST: {json.dumps(request_data, indent=2)}\n")
|
|
83
|
+
self.output_text.insert(tk.END, f"RESPONSE: {response_data}\n")
|
|
84
|
+
self.output_text.insert(tk.END, "======================\n")
|
|
85
|
+
self.output_text.see(tk.END)
|
|
86
|
+
|
|
87
|
+
def test_click(self):
|
|
88
|
+
try:
|
|
89
|
+
x = int(self.x_entry.get())
|
|
90
|
+
y = int(self.y_entry.get())
|
|
91
|
+
request_data = {"x": x, "y": y}
|
|
92
|
+
result = mcp.click_screen(**request_data)
|
|
93
|
+
self.log_output("click_screen", request_data, result)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
self.log_output("click_screen", request_data, f"Error: {str(e)}")
|
|
96
|
+
|
|
97
|
+
def test_type_text(self):
|
|
98
|
+
try:
|
|
99
|
+
text = self.text_entry.get()
|
|
100
|
+
request_data = {"text": text}
|
|
101
|
+
result = mcp.type_text(**request_data)
|
|
102
|
+
self.log_output("type_text", request_data, result)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.log_output("type_text", request_data, f"Error: {str(e)}")
|
|
105
|
+
|
|
106
|
+
def test_screenshot(self):
|
|
107
|
+
try:
|
|
108
|
+
result = mcp.take_screenshot()
|
|
109
|
+
# Convert bytes to image for preview
|
|
110
|
+
image = Image.open(io.BytesIO(result.data))
|
|
111
|
+
# Resize for preview
|
|
112
|
+
image.thumbnail((200, 150))
|
|
113
|
+
photo = ImageTk.PhotoImage(image)
|
|
114
|
+
self.screenshot_canvas.create_image(100, 75, image=photo)
|
|
115
|
+
self.screenshot_canvas.image = photo # Keep reference
|
|
116
|
+
self.log_output("take_screenshot", {}, "Screenshot taken successfully")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
self.log_output("take_screenshot", {}, f"Error: {str(e)}")
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
root = tk.Tk()
|
|
122
|
+
app = TestHarnessGUI(root)
|
|
123
|
+
root.mainloop()
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Server module for Computer Control MCP.
|
|
3
|
+
|
|
4
|
+
This module provides a simple way to run the MCP server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from computer_control_mcp.core import main as run_server
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
"""Run the MCP server."""
|
|
11
|
+
print("Starting Computer Control MCP server...")
|
|
12
|
+
run_server()
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
main()
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
import re
|
|
7
|
+
import asyncio
|
|
8
|
+
import uuid
|
|
9
|
+
import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
# --- Auto-install dependencies if needed ---
|
|
14
|
+
import pyautogui
|
|
15
|
+
from mcp.server.fastmcp import FastMCP, Image
|
|
16
|
+
import mss
|
|
17
|
+
from PIL import Image as PILImage
|
|
18
|
+
import pygetwindow as gw
|
|
19
|
+
from fuzzywuzzy import fuzz, process
|
|
20
|
+
|
|
21
|
+
import cv2
|
|
22
|
+
from rapidocr_onnxruntime import RapidOCR, VisRes
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def log(message: str) -> None:
|
|
26
|
+
"""Log a message to stderr."""
|
|
27
|
+
print(f"STDOUT: {message}", file=sys.stderr)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_downloads_dir() -> Path:
|
|
31
|
+
"""Get the OS downloads directory."""
|
|
32
|
+
if os.name == "nt": # Windows
|
|
33
|
+
import winreg
|
|
34
|
+
|
|
35
|
+
sub_key = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
|
|
36
|
+
downloads_guid = "{374DE290-123F-4565-9164-39C4925E467B}"
|
|
37
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key:
|
|
38
|
+
downloads_dir = winreg.QueryValueEx(key, downloads_guid)[0]
|
|
39
|
+
return Path(downloads_dir)
|
|
40
|
+
else: # macOS, Linux, etc.
|
|
41
|
+
return Path.home() / "Downloads"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _mss_screenshot(region=None):
|
|
45
|
+
"""Take a screenshot using mss and return PIL Image.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
region: Optional tuple (left, top, width, height) for region capture
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
PIL Image object
|
|
52
|
+
"""
|
|
53
|
+
with mss.mss() as sct:
|
|
54
|
+
if region is None:
|
|
55
|
+
# Full screen screenshot
|
|
56
|
+
monitor = sct.monitors[0] # All monitors combined
|
|
57
|
+
else:
|
|
58
|
+
# Region screenshot
|
|
59
|
+
left, top, width, height = region
|
|
60
|
+
monitor = {
|
|
61
|
+
"left": left,
|
|
62
|
+
"top": top,
|
|
63
|
+
"width": width,
|
|
64
|
+
"height": height,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
screenshot = sct.grab(monitor)
|
|
68
|
+
# Convert to PIL Image
|
|
69
|
+
return PILImage.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def save_image_to_downloads(
|
|
73
|
+
image, prefix: str = "screenshot", directory: Path = None
|
|
74
|
+
) -> Tuple[str, bytes]:
|
|
75
|
+
"""Save an image to the downloads directory and return its absolute path.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
image: Either a PIL Image object or MCP Image object
|
|
79
|
+
prefix: Prefix for the filename (default: 'screenshot')
|
|
80
|
+
directory: Optional directory to save the image to
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Tuple of (absolute_path, image_data_bytes)
|
|
84
|
+
"""
|
|
85
|
+
# Create a unique filename with timestamp
|
|
86
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
87
|
+
unique_id = str(uuid.uuid4())[:8]
|
|
88
|
+
filename = f"{prefix}_{timestamp}_{unique_id}.png"
|
|
89
|
+
|
|
90
|
+
# Get downloads directory
|
|
91
|
+
downloads_dir = directory or get_downloads_dir()
|
|
92
|
+
filepath = downloads_dir / filename
|
|
93
|
+
|
|
94
|
+
# Handle different image types
|
|
95
|
+
if hasattr(image, "save"): # PIL Image
|
|
96
|
+
image.save(filepath)
|
|
97
|
+
# Also get the bytes for returning
|
|
98
|
+
img_byte_arr = BytesIO()
|
|
99
|
+
image.save(img_byte_arr, format="PNG")
|
|
100
|
+
img_bytes = img_byte_arr.getvalue()
|
|
101
|
+
elif hasattr(image, "data"): # MCP Image
|
|
102
|
+
img_bytes = image.data
|
|
103
|
+
with open(filepath, "wb") as f:
|
|
104
|
+
f.write(img_bytes)
|
|
105
|
+
else:
|
|
106
|
+
raise TypeError("Unsupported image type")
|
|
107
|
+
|
|
108
|
+
log(f"Saved image to {filepath}")
|
|
109
|
+
return str(filepath.absolute()), img_bytes
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _find_matching_window(
|
|
113
|
+
windows: any,
|
|
114
|
+
title_pattern: str = None,
|
|
115
|
+
use_regex: bool = False,
|
|
116
|
+
threshold: int = 60,
|
|
117
|
+
) -> Optional[Dict[str, Any]]:
|
|
118
|
+
"""Helper function to find a matching window based on title pattern.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
windows: List of window dictionaries
|
|
122
|
+
title_pattern: Pattern to match window title
|
|
123
|
+
use_regex: If True, treat the pattern as a regex, otherwise use fuzzy matching
|
|
124
|
+
threshold: Minimum score (0-100) required for a fuzzy match
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The best matching window or None if no match found
|
|
128
|
+
"""
|
|
129
|
+
if not title_pattern:
|
|
130
|
+
log("No title pattern provided, returning None")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
# For regex matching
|
|
134
|
+
if use_regex:
|
|
135
|
+
for window in windows:
|
|
136
|
+
if re.search(title_pattern, window["title"], re.IGNORECASE):
|
|
137
|
+
log(f"Regex match found: {window['title']}")
|
|
138
|
+
return window
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
# For fuzzy matching using fuzzywuzzy
|
|
142
|
+
# Extract all window titles
|
|
143
|
+
window_titles = [window["title"] for window in windows]
|
|
144
|
+
|
|
145
|
+
# Use process.extractOne to find the best match
|
|
146
|
+
best_match_title, score = process.extractOne(
|
|
147
|
+
title_pattern, window_titles, scorer=fuzz.partial_ratio
|
|
148
|
+
)
|
|
149
|
+
log(f"Best fuzzy match: '{best_match_title}' with score {score}")
|
|
150
|
+
|
|
151
|
+
# Only return if the score is above the threshold
|
|
152
|
+
if score >= threshold:
|
|
153
|
+
# Find the window with the matching title
|
|
154
|
+
for window in windows:
|
|
155
|
+
if window["title"] == best_match_title:
|
|
156
|
+
return window
|
|
157
|
+
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def take_screenshot(
|
|
162
|
+
title_pattern: str = None,
|
|
163
|
+
use_regex: bool = False,
|
|
164
|
+
threshold: int = 60,
|
|
165
|
+
save_to_downloads: bool = False,
|
|
166
|
+
) -> Image:
|
|
167
|
+
"""
|
|
168
|
+
Take screenshots based on the specified title pattern and save them to the downloads directory with absolute paths returned.
|
|
169
|
+
If no title pattern is provided, take screenshot of entire screen.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
title_pattern: Pattern to match window title, if None, take screenshot of entire screen
|
|
173
|
+
use_regex: If True, treat the pattern as a regex, otherwise best match with fuzzy matching
|
|
174
|
+
save_to_downloads: If True, save the screenshot to the downloads directory and return the absolute path
|
|
175
|
+
threshold: Minimum score (0-100) required for a fuzzy match
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Always returns a single screenshot as MCP Image object, content type image not supported means preview isnt supported but Image object is there.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
all_windows = gw.getAllWindows()
|
|
182
|
+
|
|
183
|
+
# Convert to list of dictionaries for _find_matching_window
|
|
184
|
+
windows = []
|
|
185
|
+
for window in all_windows:
|
|
186
|
+
if window.title: # Only include windows with titles
|
|
187
|
+
windows.append(
|
|
188
|
+
{
|
|
189
|
+
"title": window.title,
|
|
190
|
+
"window_obj": window, # Store the actual window object
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
print(f"Found {len(windows)} windows")
|
|
195
|
+
window = _find_matching_window(windows, title_pattern, use_regex, threshold)
|
|
196
|
+
window = window["window_obj"] if window else None
|
|
197
|
+
|
|
198
|
+
# Store the currently active window
|
|
199
|
+
current_active_window = gw.getActiveWindow()
|
|
200
|
+
|
|
201
|
+
# Take the screenshot
|
|
202
|
+
if not window:
|
|
203
|
+
print("No matching window found, taking screenshot of entire screen")
|
|
204
|
+
screenshot = _mss_screenshot()
|
|
205
|
+
else:
|
|
206
|
+
print(f"Taking screenshot of window: {window.title}")
|
|
207
|
+
# Activate the window and wait for it to be fully in focus
|
|
208
|
+
window.activate()
|
|
209
|
+
pyautogui.sleep(0.5) # Wait for 0.5 seconds to ensure window is active
|
|
210
|
+
screenshot = _mss_screenshot(
|
|
211
|
+
region=(window.left, window.top, window.width, window.height)
|
|
212
|
+
)
|
|
213
|
+
# Restore the previously active window
|
|
214
|
+
if current_active_window:
|
|
215
|
+
current_active_window.activate()
|
|
216
|
+
pyautogui.sleep(0.2) # Wait a bit to ensure previous window is restored
|
|
217
|
+
|
|
218
|
+
# Create temp directory
|
|
219
|
+
temp_dir = Path(tempfile.mkdtemp())
|
|
220
|
+
|
|
221
|
+
# Save screenshot and get filepath
|
|
222
|
+
filepath, _ = save_image_to_downloads(
|
|
223
|
+
screenshot, prefix="screenshot", directory=temp_dir
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Create Image object from filepath
|
|
227
|
+
image = Image(filepath)
|
|
228
|
+
|
|
229
|
+
# Copy from temp to downloads
|
|
230
|
+
if save_to_downloads:
|
|
231
|
+
print("Copying screenshot from temp to downloads")
|
|
232
|
+
shutil.copy(filepath, get_downloads_dir())
|
|
233
|
+
|
|
234
|
+
return image # MCP Image object
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
print(f"Error taking screenshot: {str(e)}")
|
|
238
|
+
return f"Error taking screenshot: {str(e)}"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_ocr_from_screenshot(
|
|
242
|
+
title_pattern: str = None,
|
|
243
|
+
use_regex: bool = False,
|
|
244
|
+
threshold: int = 60,
|
|
245
|
+
scale_percent: int = 100,
|
|
246
|
+
) -> any:
|
|
247
|
+
"""
|
|
248
|
+
Get OCR text from the specified title pattern and save them to the downloads directory with absolute paths returned.
|
|
249
|
+
If no title pattern is provided, get all Text on the screen.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
title_pattern: Pattern to match window title, if None, get all UI elements on the screen
|
|
253
|
+
use_regex: If True, treat the pattern as a regex, otherwise best match with fuzzy matching
|
|
254
|
+
save_to_downloads: If True, save the screenshot to the downloads directory and return the absolute path
|
|
255
|
+
threshold: Minimum score (0-100) required for a fuzzy match
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
List of UI elements as MCP Image objects
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
|
|
262
|
+
all_windows = gw.getAllWindows()
|
|
263
|
+
|
|
264
|
+
# Convert to list of dictionaries for _find_matching_window
|
|
265
|
+
windows = []
|
|
266
|
+
for window in all_windows:
|
|
267
|
+
if window.title: # Only include windows with titles
|
|
268
|
+
windows.append(
|
|
269
|
+
{
|
|
270
|
+
"title": window.title,
|
|
271
|
+
"window_obj": window, # Store the actual window object
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
log(f"Found {len(windows)} windows")
|
|
276
|
+
window = _find_matching_window(windows, title_pattern, use_regex, threshold)
|
|
277
|
+
window = window["window_obj"] if window else None
|
|
278
|
+
|
|
279
|
+
# Store the currently active window
|
|
280
|
+
current_active_window = gw.getActiveWindow()
|
|
281
|
+
|
|
282
|
+
# Take the screenshot
|
|
283
|
+
if not window:
|
|
284
|
+
log("No matching window found, taking screenshot of entire screen")
|
|
285
|
+
screenshot = _mss_screenshot()
|
|
286
|
+
else:
|
|
287
|
+
log(f"Taking screenshot of window: {window.title}")
|
|
288
|
+
# Activate the window and wait for it to be fully in focus
|
|
289
|
+
window.activate()
|
|
290
|
+
pyautogui.sleep(0.5) # Wait for 0.5 seconds to ensure window is active
|
|
291
|
+
screenshot = _mss_screenshot(
|
|
292
|
+
region=(window.left, window.top, window.width, window.height)
|
|
293
|
+
)
|
|
294
|
+
# Restore the previously active window
|
|
295
|
+
if current_active_window:
|
|
296
|
+
current_active_window.activate()
|
|
297
|
+
pyautogui.sleep(0.2) # Wait a bit to ensure previous window is restored
|
|
298
|
+
|
|
299
|
+
# Create temp directory
|
|
300
|
+
temp_dir = Path(tempfile.mkdtemp())
|
|
301
|
+
|
|
302
|
+
# Save screenshot and get filepath
|
|
303
|
+
filepath, _ = save_image_to_downloads(
|
|
304
|
+
screenshot, prefix="screenshot", directory=temp_dir
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Create Image object from filepath
|
|
308
|
+
image = Image(filepath)
|
|
309
|
+
|
|
310
|
+
# Copy from temp to downloads
|
|
311
|
+
if False:
|
|
312
|
+
log("Copying screenshot from temp to downloads")
|
|
313
|
+
shutil.copy(filepath, get_downloads_dir())
|
|
314
|
+
|
|
315
|
+
image_path = image.path
|
|
316
|
+
img = cv2.imread(image_path)
|
|
317
|
+
|
|
318
|
+
# Lower down resolution before processing
|
|
319
|
+
width = int(img.shape[1] * scale_percent / 100)
|
|
320
|
+
height = int(img.shape[0] * scale_percent / 100)
|
|
321
|
+
dim = (width, height)
|
|
322
|
+
resized_img = cv2.resize(img, dim, interpolation=cv2.INTER_AREA)
|
|
323
|
+
# save resized image to pwd
|
|
324
|
+
# cv2.imwrite("resized_img.png", resized_img)
|
|
325
|
+
engine = RapidOCR()
|
|
326
|
+
vis = VisRes()
|
|
327
|
+
|
|
328
|
+
result, elapse_list = engine(resized_img)
|
|
329
|
+
boxes, txts, scores = list(zip(*result))
|
|
330
|
+
boxes = [[[x + window.left, y + window.top] for x, y in box] for box in boxes]
|
|
331
|
+
zipped_results = list(zip(boxes, txts, scores))
|
|
332
|
+
|
|
333
|
+
return zipped_results
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
log(f"Error getting UI elements: {str(e)}")
|
|
337
|
+
import traceback
|
|
338
|
+
|
|
339
|
+
stack_trace = traceback.format_exc()
|
|
340
|
+
log(f"Stack trace:\n{stack_trace}")
|
|
341
|
+
return f"Error getting UI elements: {str(e)}\nStack trace:\n{stack_trace}"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
import json
|
|
345
|
+
|
|
346
|
+
print(json.dumps(get_ocr_from_screenshot("chrome")))
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Computer Control MCP Tests
|
|
2
|
+
|
|
3
|
+
This directory contains the tests for the Computer Control MCP package.
|
|
4
|
+
|
|
5
|
+
## Running Tests
|
|
6
|
+
|
|
7
|
+
To run the tests, use pytest:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pytest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or with specific test:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pytest tests/test_computer_control.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Test Structure
|
|
20
|
+
|
|
21
|
+
- `conftest.py`: Pytest configuration
|
|
22
|
+
- `test_computer_control.py`: Tests for the core functionality
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
from rapidocr import RapidOCR
|
|
3
|
+
from rapidocr_onnxruntime import VisRes
|
|
4
|
+
|
|
5
|
+
image_path = r"C:\Users\Admin\AppData\Local\Temp\tmpdw2d8r14\screenshot_20250815_033153_f99a8396.png"
|
|
6
|
+
img = cv2.imread(image_path)
|
|
7
|
+
if img is None:
|
|
8
|
+
print(f"Failed to load img: {image_path}")
|
|
9
|
+
else:
|
|
10
|
+
print(f"Loaded img: {image_path}, shape: {img.shape}")
|
|
11
|
+
engine = RapidOCR()
|
|
12
|
+
vis = VisRes()
|
|
13
|
+
output = engine(img)
|
|
14
|
+
|
|
15
|
+
# Separate into boxes, texts, and scores
|
|
16
|
+
boxes = output.boxes
|
|
17
|
+
txts = output.txts
|
|
18
|
+
scores = output.scores
|
|
19
|
+
zipped_results = list(zip(boxes, txts, scores))
|
|
20
|
+
print(f"Found {len(zipped_results)} text items in OCR result.")
|
|
21
|
+
print(f"First 10 items: {str(zipped_results).encode("utf-8", errors="ignore")}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Simple script to run the Computer Control MCP server.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# import sys
|
|
7
|
+
# import os
|
|
8
|
+
# sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|
9
|
+
# from computer_control_mcp.core import main
|
|
10
|
+
|
|
11
|
+
from computer_control_mcp.core import main
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
print("Starting Computer Control MCP server...")
|
|
15
|
+
main()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Backward compatibility setup.py file for Computer Control MCP.
|
|
4
|
+
This file is provided for backward compatibility with tools that don't support pyproject.toml.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import setuptools
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
try:
|
|
11
|
+
setuptools.setup()
|
|
12
|
+
except Exception as e:
|
|
13
|
+
print(f"Error: {e}")
|
|
14
|
+
print("\nThis package uses pyproject.toml for configuration.")
|
|
15
|
+
print("Please use a PEP 517 compatible build tool like pip or build.")
|
|
16
|
+
print("For example: pip install .")
|