Skip to content

Commit e32b0c1

Browse files
authored
Merge pull request #12 from vlad-terin/mousedragopenapp
Mousedragopenapp
2 parents 6b9b6bf + c390757 commit e32b0c1

File tree

2 files changed

+235
-17
lines changed

2 files changed

+235
-17
lines changed

src/action_handlers.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import base64
44
import os
55
import sys
6+
import subprocess
7+
import time
68

79
import mcp.types as types
810
# Import vnc_client from the current directory
@@ -476,6 +478,181 @@ def handle_remote_macos_mouse_move(arguments: dict[str, Any]) -> list[types.Text
476478
Target dimensions: {target_width}x{target_height}
477479
Scale factors: {scale_factors['x']:.4f}x, {scale_factors['y']:.4f}y"""
478480
)]
481+
finally:
482+
# Close VNC connection
483+
vnc.close()
484+
485+
486+
def handle_remote_macos_open_application(arguments: dict[str, Any]) -> List[types.TextContent]:
487+
"""
488+
Opens or activates an application on the remote MacOS machine using VNC.
489+
490+
Args:
491+
arguments: Dictionary containing:
492+
- identifier: App name, path, or bundle ID
493+
494+
Returns:
495+
List containing a TextContent with the result
496+
"""
497+
# Use environment variables
498+
host = MACOS_HOST
499+
port = MACOS_PORT
500+
password = MACOS_PASSWORD
501+
username = MACOS_USERNAME
502+
encryption = VNC_ENCRYPTION
503+
504+
identifier = arguments.get("identifier")
505+
if not identifier:
506+
raise ValueError("identifier is required")
507+
508+
start_time = time.time()
509+
510+
# Initialize VNC client
511+
vnc = VNCClient(host=host, port=port, password=password, username=username, encryption=encryption)
512+
513+
# Connect to remote MacOs machine
514+
success, error_message = vnc.connect()
515+
if not success:
516+
error_msg = f"Failed to connect to remote MacOs machine at {host}:{port}. {error_message}"
517+
return [types.TextContent(type="text", text=error_msg)]
518+
519+
try:
520+
# Send Command+Space to open Spotlight
521+
cmd_key = 0xffeb # Command key
522+
space_key = 0x20 # Space key
523+
524+
# Press Command+Space
525+
vnc.send_key_event(cmd_key, True)
526+
vnc.send_key_event(space_key, True)
527+
528+
# Release Command+Space
529+
vnc.send_key_event(space_key, False)
530+
vnc.send_key_event(cmd_key, False)
531+
532+
# Small delay to let Spotlight open
533+
time.sleep(0.5)
534+
535+
# Type the application name
536+
vnc.send_text(identifier)
537+
538+
# Small delay to let Spotlight find the app
539+
time.sleep(0.5)
540+
541+
# Press Enter to launch
542+
enter_key = 0xff0d
543+
vnc.send_key_event(enter_key, True)
544+
vnc.send_key_event(enter_key, False)
545+
546+
end_time = time.time()
547+
processing_time = round(end_time - start_time, 3)
548+
549+
return [types.TextContent(
550+
type="text",
551+
text=f"Launched application: {identifier}\nProcessing time: {processing_time}s"
552+
)]
553+
554+
finally:
555+
# Close VNC connection
556+
vnc.close()
557+
558+
559+
def handle_remote_macos_mouse_drag(arguments: dict[str, Any]) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
560+
"""Perform a mouse drag operation on a remote MacOs machine."""
561+
# Use environment variables
562+
host = MACOS_HOST
563+
port = MACOS_PORT
564+
password = MACOS_PASSWORD
565+
username = MACOS_USERNAME
566+
encryption = VNC_ENCRYPTION
567+
568+
# Get required parameters from arguments
569+
start_x = arguments.get("start_x")
570+
start_y = arguments.get("start_y")
571+
end_x = arguments.get("end_x")
572+
end_y = arguments.get("end_y")
573+
source_width = int(arguments.get("source_width", 1366))
574+
source_height = int(arguments.get("source_height", 768))
575+
button = int(arguments.get("button", 1))
576+
steps = int(arguments.get("steps", 10))
577+
delay_ms = int(arguments.get("delay_ms", 10))
578+
579+
# Validate required parameters
580+
if any(x is None for x in [start_x, start_y, end_x, end_y]):
581+
raise ValueError("start_x, start_y, end_x, and end_y coordinates are required")
582+
583+
# Ensure source dimensions are positive
584+
if source_width <= 0 or source_height <= 0:
585+
raise ValueError("Source dimensions must be positive values")
586+
587+
# Initialize VNC client
588+
vnc = VNCClient(host=host, port=port, password=password, username=username, encryption=encryption)
589+
590+
# Connect to remote MacOs machine
591+
success, error_message = vnc.connect()
592+
if not success:
593+
error_msg = f"Failed to connect to remote MacOs machine at {host}:{port}. {error_message}"
594+
return [types.TextContent(type="text", text=error_msg)]
595+
596+
try:
597+
# Get target screen dimensions
598+
target_width = vnc.width
599+
target_height = vnc.height
600+
601+
# Scale coordinates
602+
scaled_start_x = int((start_x / source_width) * target_width)
603+
scaled_start_y = int((start_y / source_height) * target_height)
604+
scaled_end_x = int((end_x / source_width) * target_width)
605+
scaled_end_y = int((end_y / source_height) * target_height)
606+
607+
# Ensure coordinates are within the screen bounds
608+
scaled_start_x = max(0, min(scaled_start_x, target_width - 1))
609+
scaled_start_y = max(0, min(scaled_start_y, target_height - 1))
610+
scaled_end_x = max(0, min(scaled_end_x, target_width - 1))
611+
scaled_end_y = max(0, min(scaled_end_y, target_height - 1))
612+
613+
# Calculate step sizes
614+
dx = (scaled_end_x - scaled_start_x) / steps
615+
dy = (scaled_end_y - scaled_start_y) / steps
616+
617+
# Move to start position
618+
if not vnc.send_pointer_event(scaled_start_x, scaled_start_y, 0):
619+
return [types.TextContent(type="text", text="Failed to move to start position")]
620+
621+
# Press button
622+
button_mask = 1 << (button - 1)
623+
if not vnc.send_pointer_event(scaled_start_x, scaled_start_y, button_mask):
624+
return [types.TextContent(type="text", text="Failed to press mouse button")]
625+
626+
# Perform drag
627+
for step in range(1, steps + 1):
628+
current_x = int(scaled_start_x + dx * step)
629+
current_y = int(scaled_start_y + dy * step)
630+
if not vnc.send_pointer_event(current_x, current_y, button_mask):
631+
return [types.TextContent(type="text", text=f"Failed during drag at step {step}")]
632+
time.sleep(delay_ms / 1000.0) # Convert ms to seconds
633+
634+
# Release button at final position
635+
if not vnc.send_pointer_event(scaled_end_x, scaled_end_y, 0):
636+
return [types.TextContent(type="text", text="Failed to release mouse button")]
637+
638+
# Prepare the response with useful details
639+
scale_factors = {
640+
"x": target_width / source_width,
641+
"y": target_height / source_height
642+
}
643+
644+
return [types.TextContent(
645+
type="text",
646+
text=f"""Mouse drag (button {button}) completed:
647+
From source ({start_x}, {start_y}) to ({end_x}, {end_y})
648+
From target ({scaled_start_x}, {scaled_start_y}) to ({scaled_end_x}, {scaled_end_y})
649+
Source dimensions: {source_width}x{source_height}
650+
Target dimensions: {target_width}x{target_height}
651+
Scale factors: {scale_factors['x']:.4f}x, {scale_factors['y']:.4f}y
652+
Steps: {steps}
653+
Delay: {delay_ms}ms"""
654+
)]
655+
479656
finally:
480657
# Close VNC connection
481658
vnc.close()

src/mcp_remote_macos_use/server.py

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
handle_remote_macos_send_keys,
3737
handle_remote_macos_mouse_move,
3838
handle_remote_macos_mouse_click,
39-
handle_remote_macos_mouse_double_click
39+
handle_remote_macos_mouse_double_click,
40+
handle_remote_macos_open_application,
41+
handle_remote_macos_mouse_drag
4042
)
4143

4244
# Configure logging
@@ -82,12 +84,12 @@
8284
async def main():
8385
"""Run the Remote MacOS MCP server."""
8486
logger.info("Remote MacOS computer use server starting")
85-
87+
8688
# Initialize LiveKit handler if environment variables are set
8789
livekit_handler = None
8890
if all([LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET]):
8991
livekit_handler = LiveKitHandler()
90-
92+
9193
# Generate access token for the room
9294
token = api.AccessToken() \
9395
.with_identity("remote-macos-bot") \
@@ -96,7 +98,7 @@ async def main():
9698
room_join=True,
9799
room="remote-macos-room",
98100
)).to_jwt()
99-
101+
100102
# Start LiveKit connection
101103
success = await livekit_handler.start("remote-macos-room", token)
102104
if success:
@@ -113,7 +115,7 @@ async def main():
113115
if not MACOS_PASSWORD:
114116
logger.error("MACOS_PASSWORD environment variable is required but not set")
115117
raise ValueError("MACOS_PASSWORD environment variable is required but not set")
116-
118+
117119
server = Server("remote-macos-client")
118120

119121
@server.list_resources()
@@ -147,8 +149,8 @@ async def handle_list_tools() -> list[types.Tool]:
147149
"source_width": {"type": "integer", "description": "Width of the reference screen for coordinate scaling", "default": 1366},
148150
"source_height": {"type": "integer", "description": "Height of the reference screen for coordinate scaling", "default": 768},
149151
"direction": {
150-
"type": "string",
151-
"description": "Scroll direction",
152+
"type": "string",
153+
"description": "Scroll direction",
152154
"enum": ["up", "down"],
153155
"default": "down"
154156
}
@@ -213,6 +215,39 @@ async def handle_list_tools() -> list[types.Tool]:
213215
"required": ["x", "y"]
214216
},
215217
),
218+
types.Tool(
219+
name="remote_macos_open_application",
220+
description="Opens/activates an application and returns its PID for further interactions.",
221+
inputSchema={
222+
"type": "object",
223+
"properties": {
224+
"identifier": {
225+
"type": "string",
226+
"description": "REQUIRED. App name, path, or bundle ID."
227+
}
228+
},
229+
"required": ["identifier"]
230+
},
231+
),
232+
types.Tool(
233+
name="remote_macos_mouse_drag",
234+
description="Perform a mouse drag operation from start point to end point on a remote MacOs machine, with automatic coordinate scaling. Uses environment variables for connection details.",
235+
inputSchema={
236+
"type": "object",
237+
"properties": {
238+
"start_x": {"type": "integer", "description": "Starting X coordinate (in source dimensions)"},
239+
"start_y": {"type": "integer", "description": "Starting Y coordinate (in source dimensions)"},
240+
"end_x": {"type": "integer", "description": "Ending X coordinate (in source dimensions)"},
241+
"end_y": {"type": "integer", "description": "Ending Y coordinate (in source dimensions)"},
242+
"source_width": {"type": "integer", "description": "Width of the reference screen for coordinate scaling", "default": 1366},
243+
"source_height": {"type": "integer", "description": "Height of the reference screen for coordinate scaling", "default": 768},
244+
"button": {"type": "integer", "description": "Mouse button (1=left, 2=middle, 3=right)", "default": 1},
245+
"steps": {"type": "integer", "description": "Number of intermediate points for smooth dragging", "default": 10},
246+
"delay_ms": {"type": "integer", "description": "Delay between steps in milliseconds", "default": 10}
247+
},
248+
"required": ["start_x", "start_y", "end_x", "end_y"]
249+
},
250+
),
216251
]
217252

218253
@server.call_tool()
@@ -223,25 +258,31 @@ async def handle_call_tool(
223258
try:
224259
if not arguments:
225260
arguments = {}
226-
261+
227262
if name == "remote_macos_get_screen":
228263
return await handle_remote_macos_get_screen(arguments)
229-
264+
230265
elif name == "remote_macos_mouse_scroll":
231-
return handle_remote_macos_mouse_scroll(arguments)
232-
266+
return handle_remote_macos_mouse_scroll(arguments)
267+
233268
elif name == "remote_macos_send_keys":
234269
return handle_remote_macos_send_keys(arguments)
235-
270+
236271
elif name == "remote_macos_mouse_move":
237272
return handle_remote_macos_mouse_move(arguments)
238-
273+
239274
elif name == "remote_macos_mouse_click":
240275
return handle_remote_macos_mouse_click(arguments)
241-
276+
242277
elif name == "remote_macos_mouse_double_click":
243278
return handle_remote_macos_mouse_double_click(arguments)
244-
279+
280+
elif name == "remote_macos_open_application":
281+
return handle_remote_macos_open_application(arguments)
282+
283+
elif name == "remote_macos_mouse_drag":
284+
return handle_remote_macos_mouse_drag(arguments)
285+
245286
else:
246287
raise ValueError(f"Unknown tool: {name}")
247288

@@ -271,7 +312,7 @@ async def handle_call_tool(
271312
if __name__ == "__main__":
272313
# Load environment variables from .env file if it exists
273314
load_dotenv()
274-
315+
275316
try:
276317
# Run the server
277318
asyncio.run(main())
@@ -282,4 +323,4 @@ async def handle_call_tool(
282323
except Exception as e:
283324
logger.error(f"Unexpected error: {str(e)}", exc_info=True)
284325
print(f"ERROR: Unexpected error occurred: {str(e)}")
285-
sys.exit(1)
326+
sys.exit(1)

0 commit comments

Comments
 (0)