feat: 30 MCP tools — RDP click, type, clipboard interaction
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 4m5s

3 new MCP tools completing the RDP interaction loop:

- rdp_click — click at x,y coordinates (left/right/middle button)
  Use terminal_screenshot first to identify coordinates
- rdp_type — type text into RDP session via clipboard paste
- rdp_clipboard — set clipboard content on remote desktop

The AI can now screenshot an RDP session, analyze it visually,
click buttons, type text, and read clipboard content. Full GUI
automation through the MCP bridge.

Total MCP tools: 30.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-26 17:01:22 -04:00
parent 3c2dc435ff
commit 5aaedbe4a5
2 changed files with 59 additions and 0 deletions

View File

@ -264,6 +264,21 @@ fn handle_tools_list(id: Value) -> JsonRpcResponse {
"description": "Show recent commits on a remote repository",
"inputSchema": { "type": "object", "properties": { "session_id": { "type": "string" }, "path": { "type": "string" } }, "required": ["session_id", "path"] }
},
{
"name": "rdp_click",
"description": "Click at a position in an RDP session (use terminal_screenshot first to see coordinates)",
"inputSchema": { "type": "object", "properties": { "session_id": { "type": "string" }, "x": { "type": "number" }, "y": { "type": "number" }, "button": { "type": "string", "description": "left (default), right, or middle" } }, "required": ["session_id", "x", "y"] }
},
{
"name": "rdp_type",
"description": "Type text into an RDP session via clipboard paste",
"inputSchema": { "type": "object", "properties": { "session_id": { "type": "string" }, "text": { "type": "string" } }, "required": ["session_id", "text"] }
},
{
"name": "rdp_clipboard",
"description": "Set the clipboard content on a remote RDP session",
"inputSchema": { "type": "object", "properties": { "session_id": { "type": "string" }, "text": { "type": "string" } }, "required": ["session_id", "text"] }
},
{
"name": "list_sessions",
"description": "List all active Wraith sessions (SSH, RDP, PTY) with connection details",
@ -329,6 +344,9 @@ fn handle_tool_call(id: Value, port: u16, tool_name: &str, args: &Value) -> Json
"git_status" => call_wraith(port, "/mcp/git/status", args.clone()),
"git_pull" => call_wraith(port, "/mcp/git/pull", args.clone()),
"git_log" => call_wraith(port, "/mcp/git/log", args.clone()),
"rdp_click" => call_wraith(port, "/mcp/rdp/click", args.clone()),
"rdp_type" => call_wraith(port, "/mcp/rdp/type", args.clone()),
"rdp_clipboard" => call_wraith(port, "/mcp/rdp/clipboard", args.clone()),
"terminal_screenshot" => {
let result = call_wraith(port, "/mcp/screenshot", args.clone());
// Screenshot returns base64 PNG — wrap as image content for multimodal AI

View File

@ -432,6 +432,44 @@ async fn handle_git_log(AxumState(state): AxumState<Arc<McpServerState>>, Json(r
match tool_exec(&session.handle, &format!("cd {} && git log --oneline -20 2>&1", req.path)).await { Ok(o) => ok_response(o), Err(e) => err_response(e) }
}
// ── RDP interaction handlers ─────────────────────────────────────────────────
#[derive(Deserialize)]
struct RdpClickRequest { session_id: String, x: u16, y: u16, button: Option<String> }
#[derive(Deserialize)]
struct RdpTypeRequest { session_id: String, text: String }
#[derive(Deserialize)]
struct RdpClipboardRequest { session_id: String, text: String }
async fn handle_rdp_click(AxumState(state): AxumState<Arc<McpServerState>>, Json(req): Json<RdpClickRequest>) -> Json<McpResponse<String>> {
use crate::rdp::input::mouse_flags;
let button_flag = match req.button.as_deref().unwrap_or("left") {
"right" => mouse_flags::BUTTON2,
"middle" => mouse_flags::BUTTON3,
_ => mouse_flags::BUTTON1,
};
// Move to position
if let Err(e) = state.rdp.send_mouse(&req.session_id, req.x, req.y, mouse_flags::MOVE) { return err_response(e); }
// Click down
if let Err(e) = state.rdp.send_mouse(&req.session_id, req.x, req.y, button_flag | mouse_flags::DOWN) { return err_response(e); }
// Click up
if let Err(e) = state.rdp.send_mouse(&req.session_id, req.x, req.y, button_flag) { return err_response(e); }
ok_response(format!("clicked ({}, {})", req.x, req.y))
}
async fn handle_rdp_type(AxumState(state): AxumState<Arc<McpServerState>>, Json(req): Json<RdpTypeRequest>) -> Json<McpResponse<String>> {
// Type text by sending it via clipboard paste (most reliable for arbitrary text)
if let Err(e) = state.rdp.send_clipboard(&req.session_id, &req.text) { return err_response(e); }
ok_response(format!("typed {} chars", req.text.len()))
}
async fn handle_rdp_clipboard(AxumState(state): AxumState<Arc<McpServerState>>, Json(req): Json<RdpClipboardRequest>) -> Json<McpResponse<String>> {
if let Err(e) = state.rdp.send_clipboard(&req.session_id, &req.text) { return err_response(e); }
ok_response("clipboard set".to_string())
}
/// Start the MCP HTTP server and write the port to disk.
pub async fn start_mcp_server(
ssh: SshService,
@ -469,6 +507,9 @@ pub async fn start_mcp_server(
.route("/mcp/git/status", post(handle_git_status))
.route("/mcp/git/pull", post(handle_git_pull))
.route("/mcp/git/log", post(handle_git_log))
.route("/mcp/rdp/click", post(handle_rdp_click))
.route("/mcp/rdp/type", post(handle_rdp_type))
.route("/mcp/rdp/clipboard", post(handle_rdp_clipboard))
.with_state(state);
let listener = TcpListener::bind("127.0.0.1:0").await