Repository navigation
fix(tools): accept JSON-string arrays for Snapshot/Screenshot display and App window_loc/window_size - #545
Merged
Jeomon merged 1 commit intoNov 4, 2026
Conversation
… and App window_loc/window_size MCP clients that strip the anyOf schema of optional parameters make the model send a list argument as its JSON text, display="[0, 1]" instead of display=[0, 1] (CursorTouch#272). CursorTouch#277 made loc, locs and labels accept that form, and region (CursorTouch#388) and App's args (CursorTouch#335) later took it as well, but display on Snapshot and Screenshot and window_loc and window_size on App were still declared list-only. FastMCP rejected any call that sent one of them as text with "Input should be a valid list" before the tool ran. Declare the four parameters as array | string like the others, and parse a string with a new as_int_list() next to as_bool() in desktop.utils. A string must hold a JSON array of integers; anything else is rejected with an error that names the parameter. Lists pass through unchanged, and Desktop.parse_display_selection() still rejects raw strings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Some MCP clients strip the
anyOfschema of optional parameters, so the model sends a list argument as its JSON text:display="[0, 1]"instead ofdisplay=[0, 1]. #272 described this forlocand noted that the same fix applies to "any other optional list / dict params". #277 then madeloc,locsandlabelsaccept a string, andregion(#388) and App'sargs(#335) were later made to accept one as well. Four list parameters still do not:displayon Snapshot and Screenshot, andwindow_locandwindow_sizeon App. They are declaredlist[int] | None, so their schema has no string branch, unlikeregionnext to them:FastMCP rejects a call that sends one of them as text before the tool runs. For a client that sends lists as text,
displaycannot pick monitors, and App'sresizemode cannot be given a position or a size.regionsent as text is accepted, but a call that also sendsdisplayas text fails.Reproduced at 92f5e8c on Windows 11 with Python 3.14.9, fastmcp 4.1.3 and pydantic 2.13.5. I registered the App, Snapshot/Screenshot and input tools on a
FastMCPinstance and called them in-process withawait mcp.call_tool(...), so the arguments went through FastMCP's own validation. The desktop was a fake that only records calls, andcapture_desktop_stateandbuild_snapshot_responseintools.snapshotwere replaced by a recorder and a stub, so nothing was captured, moved or resized:Scrollwithloc="[10, 20]": OK, the desktop gets[10, 20](_as_loc).Snapshotwithdisplay=[0]: OK.Snapshotwithdisplay="[0]"fails, anddisplay="[0, 1]"fails the same way:Screenshotwithdisplay="[1]": the samelist_typeerror forcall[_screenshot_tool].Appwithmode="resize",window_loc=[0, 0]andwindow_size=[800, 600]: OK.window_loc="[0, 0]"andwindow_size="[800, 600]":2 validation errors for call[app_tool],list_typefor both.SnapshotandScreenshotwithregion="[0, 0, 100, 100]": OK, the capture gets[0, 0, 100, 100]. Withdisplay="[0]"added to the same call, it fails ondisplay.Fix
list[int] | str | None, likelocandregion.as_int_list(value, name)indesktop/utils.py, next toas_bool(), parses the string.tools/snapshot.pyandtools/app.pyimport it as_as_int_list, the way other tools importas_bool.Noneand lists are returned unchanged (FastMCP has already validated a list againstlist[int]). A string has to hold a JSON array of integers; anything else, such as"[0,","0",'["0"]'or"[true]", raisesValueError("<name> must be a JSON array of integers"). A malformed value is reported rather than dropped, and the string form cannot handresizeanything the list form would reject:'["a", "b"]'would otherwise only fail insideresize_app(), after the window lookup.displaywhere they already parseregion, inside theirtry, so a bad value comes back through each tool's existing message (Error capturing desktop state: display must be a JSON array of integers. Please try again.from Snapshot,Error capturing screenshot: ...from Screenshot). App parses after thelaunch_executablebranch, which still rejectswindow_locandwindow_sizeas before.Desktop.parse_display_selection()is unchanged and still rejects raw strings (test_rejects_invalid_display_valuesintests/test_snapshot_display_filter.py).With the change, in the same setup, the four schemas include
{"type": "string"}.display="[0]","[0, 1]"and (Screenshot)"[1]"reach the capture as[0],[0, 1]and[1], App gets[0, 0]and[800, 600], and the call with bothregionanddisplayas text works. Lists pass unchanged.display="[0,"returns the message above without capturing, andwindow_loc="[0,"or'["a", "b"]'givesError calling tool 'App': window_loc must be a JSON array of integerswithout reaching the desktop.Tests
New
tests/test_tool_list_arguments.py, a sibling oftests/test_tool_boolean_flags.py. It calls the tools through a realFastMCPwithClient(...).call_tool(..., raise_on_error=False), astests/test_app_switch_foreground.pydoes, because the failure is in FastMCP's argument validation, which theFakeMCPintests/test_app_tool.pyskips. The desktop is aFakeDesktopthat recordsapp()calls, and the fixtures monkeypatchcapture_desktop_stateintools.snapshotwith a recorder andbuild_snapshot_responsewith a stub.register()rebindssnapshot.state_toolandsnapshot.screenshot_tool, whichwindows_mcp.__main__hands totests/test_snapshot_display_filter.py, so the fixture puts them back afterwards.test_display_accepts_json_text, for Snapshot and Screenshot:"[0]"and"[0, 1]"reach the capture as lists, and[1]andNonepass unchanged.test_display_text_that_is_not_an_array_of_integers_is_reported:"[0,","0",'["0"]'and"[true]"return the error and capture nothing.test_app_resize_accepts_json_text:"[10, 20]"/"[800, 600]"and the list form both reachdesktop.app()as[10, 20]and[800, 600].test_app_text_that_is_not_an_array_of_integers_is_reported:"[10,","10"and'["10", "20"]', for each of the two parameters, are MCP errors that name the parameter, anddesktop.app()is not called.19 of the 24 cases fail on
mainwith thelist_typeerror, and all 24 pass with this change. The 5 that pass on both are the list andNonecases.tests/test_app_tool.py(16 passed),tests/test_snapshot_display_filter.py(37 passed),tests/test_app_switch_foreground.py(9 passed) andtests/test_desktop_utils.py(9 passed): the same onmainand on this branch. I ran these files one at a time, not the whole suite.The branch is rebased on
mainat 92f5e8c, which includes feat(snapshot): add save_path to write the screenshot to a PNG file #533 (save_path, which also editstools/snapshot.py), and merges without conflicts with the open fix(clipboard): retry OpenClipboard while another process holds the clipboard #527-feat(safety): focus the confirmation dialog, catch the kill-switch hotkey in the input hook, Esc answers No #531 (feat(safety): check launches against the resolved executable, including .lnk targets #529-feat(safety): focus the confirmation dialog, catch the kill-switch hotkey in the input hook, Esc answers No #531 also edittools/app.py).ruff checkis clean on the four files, andruff format --checkis clean ontools/app.pyand the new test file. Fordesktop/utils.pyandtools/snapshot.py,ruff format --checkreports the existing formatting (single quotes,except (AttributeError, Exception):, blank lines), the same as onmain. It does not change the new lines, and I did not reformat the others.Not tested: a live session with a client that strips the schema. The tests send the string form that #272 and #342 recorded for
loc('[1000, 600]','[500, 500]'). I did not capture the screen or move or resize real windows.