From bb685366f076fdd05d49cba1db0d03aa598bb208 Mon Sep 17 00:00:00 2001 From: Jeff Skerker <7691216+skerker@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:15:30 -0700 Subject: [PATCH] feat(mcp): optional path argument on grab_widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grab_widget MCP tool always wrote its PNG to a generated name under tempfile.gettempdir(), even though the underlying raw grab verb already accepts a path. Screenshots are evidence the human operator wants to find later (before/after grabs, bug-report captures), so the assistant needed an extra shell step to copy each capture out of the temp dir. Add an optional 'path' property to the grab_widget input schema and use it in place of the generated temp path when supplied. Everything downstream is unchanged: the request dict, the resp.get("path", out) resolution (the app-side reply stays authoritative if the app normalizes the path), and the inline/too-large branches. Test: two new field-mapping cases — caller-supplied path is emitted verbatim, and the no-path default still lands under the temp dir. Fixes #4249 Co-Authored-By: Claude Fable 5 --- docs/automation-bridge.md | 3 ++- tools/aether_mcp.py | 9 +++++++-- tools/test_aether_mcp.py | 9 +++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/automation-bridge.md b/docs/automation-bridge.md index 50690d903..8c38cb2e9 100644 --- a/docs/automation-bridge.md +++ b/docs/automation-bridge.md @@ -126,7 +126,8 @@ contributors to self-verify UI changes before requesting review. - **Windows**: use `python` (or `py -3`) instead of `python3`. **Tools exposed** (22 typed tools): introspection — `bridge_status`, -`dump_tree` (with a `filter` arg), `grab_widget` (PNG inline), +`dump_tree` (with a `filter` arg), `grab_widget` (PNG inline; optional +`path` for where the PNG is written, else a temp file), `get_state`, `get_log`, `floors`, `streams`; driving — `invoke` (on a target-not-found failure it appends `did_you_mean` candidates), `shortcut`, `tune`, `slice`, `pan`, `record`, `mark`, `window`, `menu`; diff --git a/tools/aether_mcp.py b/tools/aether_mcp.py index 24c7cbba3..2b5da1539 100644 --- a/tools/aether_mcp.py +++ b/tools/aether_mcp.py @@ -216,6 +216,10 @@ def prune_tree(node, needle): "target": {"type": "string"}, "selector": {"type": "string", "description": "pan index, only for pan/pan-visible/pan-composite"}, + "path": {"type": "string", + "description": ("output PNG path; defaults to a temp file. The " + "returned JSON's `path` is authoritative — the app " + "may normalize it")}, }, "required": ["target"]}, }, { @@ -579,8 +583,9 @@ def handle_tool(name, args): if name == "grab_widget": target = args["target"] - out = os.path.join(tempfile.gettempdir(), - f"aether-mcp-grab-{int(time.time())}.png") + out = (args.get("path") + or os.path.join(tempfile.gettempdir(), + f"aether-mcp-grab-{int(time.time())}.png")) req = {"cmd": "grab", "target": target, "path": out} if args.get("selector"): req["selector"] = str(args["selector"]) diff --git a/tools/test_aether_mcp.py b/tools/test_aether_mcp.py index f53c76b5b..6fe1ca991 100644 --- a/tools/test_aether_mcp.py +++ b/tools/test_aether_mcp.py @@ -15,6 +15,7 @@ import json import os import sys +import tempfile sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import aether_mcp # noqa: E402 @@ -82,6 +83,14 @@ def test_field_mapping(): r = reqs[0] check("grab_widget sends cmd=grab + target", r.get("cmd") == "grab" and r.get("target") == "SpectrumWidget", str(r)) + check("grab_widget default path lands in the temp dir", + r.get("path", "").startswith(tempfile.gettempdir()), str(r)) + + reqs = run_tool("grab_widget", {"target": "SpectrumWidget", + "path": "/tmp/shot.png"}) + r = reqs[0] + check("grab_widget honors caller-supplied path (#4249)", + r.get("path") == "/tmp/shot.png", str(r)) reqs = run_tool("dump_tree", {}) check("dump_tree sends cmd=dumpTree", reqs[-1].get("cmd") == "dumpTree", str(reqs))