Local MCP server giving Claude Desktop eyes and hands on macOS (OCR-first computer use).
Choto has 12 tools with mostly complete descriptions (194-char baseline met for most), but critical gaps in schema documentation and parameter descriptions. observe and execute_plan have excellent, detailed descriptions (500+ chars each) explaining complex UI semantics. However, 8 of 12 tools lack visible input parameter schemas in the source code provided, only tool names and descriptions are evident. mission_* tools and submit have minimal parameter documentation. No error handling guidance visible (e.g., what to do if a target is not found, how to retry). Tool naming is clear and verb-forward (observe, execute_plan, focus_app, recall), but composition could be tighter: mission_start/mission_status/mission_mark/mission_finish form a tightly coupled subsystem that might benefit from a single mission_manage tool or clearer separation. Output schemas are not documented for any tool.
Write names to icons that have no tooltip. This is how you label icons that cannot be named by hovering. You supply a batch of icon names: each one is a phash (the icon's visual hash, from observe) and a label (the name you want written). Choto writes them to the interface graph and returns a report of what was written, what was skipped because the icon is already named, and what failed.
Execute a batch of semantic UI steps and return a compact journey. Each step is an object: {action, target?, path?, modifiers?, text?, keys?, amount?, app_name?, from_text?, to_text?, scroll?, until_readout?, skip_if_absent?, expect?, timeout_ms?}. action is one of click, double_click, right_click, drag, type, hotkey, scroll, wait, focus_app, read, read_clipboard. target={text, scope?, region?, window?, scroll_to_find?} selects an element by its visible text (region is one of top/bottom/left/right/center to disambiguate duplicates); matching is exact, then fuzzy, then semantic. click/double_click/right_click need a target; drag needs path; type needs text; hotkey needs keys (e.g. ['cmd','shift','4']); scroll needs amount (positive scrolls down; add a target to scroll over it); focus_app needs app_name; read takes an optional target (for scope/window/region) and read_clipboard takes nothing. BRINGING TEXT BACK: every other action changes the screen, these two hand you its contents. {action:'read'} returns the text of the window the step works in — laid out as it is drawn, so the columns of a table stay columns — in an 'extracted (step N, window "...")' block after the journey. It is never truncated: read what you need, all of it. On a read, target only says WHERE to read and its text must be EMPTY — {action:'read', target:{text:'', window:'Invoice'}} reads another window, text:'' with scope:'chrome' reads the menu bar or an open menu, and region reads one part of the pane. A read has no element to aim at, so naming one is refused rather than ignored; to bound the reading by text, use from_text/to_text. from_text and to_text narrow the reading to what lies between two pieces of visible text, both lines included — the way you would say 'the table from Subtotal down to Total'. They are matched like any target, and text that is not there fails the step like any missing target. scroll:true reads a document to its END instead of the screenful in front of you: the window is scrolled and re-read until nothing new comes up, the pieces are joined on their overlap with no duplicated lines, and the position is put back where the user had it. Use it for a long page, a chat history, a log; leave it off (the default) for a pane that is fully visible, since each screenful costs a parse. Those scrolls count against the plan's action budget, and the journey says how far it got and why it stopped. {action:'read_clipboard'} returns whatever text is on the system clipboard — the way to harvest text an app will not show in full: select it and copy it with ordinary steps ({action:'hotkey', keys:['cmd','a']}, then {action:'hotkey', keys:['cmd','c']}), then read_clipboard. An empty clipboard or one holding no text is reported in the journey, not treated as a failure. Choto never writes the clipboard, so anything you copy replaces what the user had there — copy only when you need to. STEPS THAT MAY NOT BE THERE: skip_if_absent:true (only on a step with a target) turns 'target not found' from a failure into a skipped step — the journey says 'skipped: target absent', the plan carries on, and the step's expect is not checked because nothing happened. This is for the noise of a real desktop: a cookie banner, an update prompt, a 'what's new' sheet that may or may not appear. Only use it where both branches end on the same screen; a step the plan actually depends on must be allowed to fail. GESTURES (drag): use it for what a click cannot express — rubber-band selection of several items, dragging a file onto a folder or into another pane, moving a slider, resizing a window or a column. path is a list of at least two waypoints: the button goes down on the first, travels through the rest and is released on the last (two is the usual case; more describe a bent route). Each waypoint is EITHER {target:{text,...}, offset?:{dx,dy}} — an element, optionally displaced by dx pixels right and dy pixels down from its center — OR {x, y}, a raw position in the same pixel coordinates the element listing reports. Prefer targets: they are resolved against the screen as it is when the gesture runs, so a list that scrolled still works. offset is how you name a spot with no text on it, which is what a rubber-band selection starts from: to select a block of files, start at the empty margin above-left of the first one, e.g. {target:{text:'report.pdf'}, offset:{dx:-40, dy:-25}}, and end past the last one, {target:{text:'notes.txt'}, offset:{dx:60, dy:25}}. All waypoints must be in the same window, and every one of them is resolved BEFORE anything is pressed: if one cannot be found the step escalates without the gesture starting. Be deliberate — a drag is undone far less often than a click: it moves files, drops them into folders, and reorders things, and there is no generic undo for that. DRAGGING TO A VALUE (until_readout): when what you want is a READING and not a distance — a playhead at 00:12:00, a slider at 50%, a level at -6 dB — do not compute pixels. Add until_readout={watch:{text,...}, value, tolerance?} to the drag: the button goes down on path[0], the pointer is pushed toward path[1] (which is only a DIRECTION and a first guess at how far, not the destination), and after every push the executor reads the instrument and corrects itself until it shows value. The path must be exactly two points. watch names the readout ITSELF, not a label beside it, and it has to be on screen before the gesture starts; value is written the way the app writes it (00:12:00, 50%, -6 dB, 1250) and every reading is then read in that same notation. tolerance (default 0, exact) is in those units — widen it for a control that skips the exact number. The journey says how many pushes it took and what the instrument ended on; if the reading cannot be read, or never arrives, the button is released where it stands and you get the last text that was there. modifiers=['cmd'|'shift'|'alt'|'ctrl'] are held for the whole action and work on click/double_click/right_click/drag: cmd-click to add one item to a selection, shift-click to extend it to a range, alt-drag to copy in Finder instead of moving. SURFACES: a target is looked for on exactly one of two surfaces, and they never mix. target.scope='window' (the default) searches only inside the window the step works in; target.scope='chrome' searches only the menu bar, status items and any open menu or popover. So clicking a menu item is two chrome steps: click {text:'File', scope:'chrome'}, then click {text:'Save As…', scope:'chrome'}. A target missing from the surface you named fails the step and the reply says which surface was searched and which was not — it is never answered from the other one, because the same text is often on both (a keypad '5' and a menu-bar '5'). With scope='chrome' target.window and target.scroll_to_find are rejected: chrome belongs to no window and is not scrolled. Every step works inside ONE window: by default the frontmost window of the app. target.window is a substring of a window's title (case-insensitive) and picks a different one — use it when the app has several windows open and the one you mean is not in front. If no open window matches, the step fails and the reply lists the titles that are open; it never falls back to another window. OFF-SCREEN TARGETS: if the text is not visible, the step
Input schemas not visible in source for 10 of 12 tools. Only tool names and descriptions are evident; parameter types, constraints, and required/optional flags are not documented in the provided code.
Output schemas not documented for any tool. LLMs cannot plan downstream calls or extract required fields (e.g., what does observe return? What fields does execute_plan's journey contain?).
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | F | 49 | <=2025-11-25 | v2 |
Bring an app to the front and return its frontmost window. This is how you switch to a different app without a click. You supply the app name (e.g. 'Finder', 'Safari', 'Terminal'). Choto activates the app, waits for it to come to the front, and returns the title of its frontmost window. If the app is not running, it is launched. If the app name is ambiguous (e.g. 'Mail' could be Apple Mail or another mail client), the error says which apps match.
End the active mission and archive it. Choto marks the mission as finished in the database and returns a summary of what was completed.
Mark a mission item as complete or incomplete. You supply the item text and whether it is done. Choto updates the mission in the database and returns the new status.
Replay a past mission: re-read the windows it visited and return what changed. You supply the mission ID. Choto re-reads each window the mission touched, compares the current state to what was recorded, and returns a report of what changed (text added, removed, or modified).
Start a new mission (a checklist of tasks to complete). You supply a title and a list of items. Choto creates a new mission in the database and returns its ID. The mission is initially empty; you add items to it with mission_mark.
Get the current status of the active mission. Returns the mission title, the list of items, and which ones are marked complete.
Capture the frontmost window and return a screenshot plus a textual listing of its text elements. Use this once before planning a batch of actions. TWO SURFACES: the elements come in two labelled blocks, because a step searches exactly one of them. 'elements in focus window (N)' is the focus window — the default search space of every step, and all a plain target can reach; text in a background window is deliberately not listed and cannot be targeted. 'chrome (menu bar / system, M)' is the macOS menu bar, status items and any open menu or popover; a step reaches those only with target.scope='chrome' — including the step right after opening a menu, since the dropped-down menu is chrome too, not part of the window. The chrome block is printed even when it is empty (M=0), with the reason. The reply also contains: the frontmost app name; which window was read (window); every window that app has open (open_windows) — quote a piece of one of those titles as target.window in execute_plan to work in a window other than the frontmost; whether this window is already known to the interface graph (known_window); the screen size in both pixels (OCR/element coordinates) and logical points (click coordinates); which area was scanned for single-character labels (single_character_scan); and, when there is text on neither surface, how much of it there is (off_surface — counted, not listed, because no step can act on it). Each element reads 'text @ (cx,cy) [conf 0.97]' where (cx,cy) is the element center in pixels. Element text is what you target in execute_plan. A PNG screenshot of the whole display is attached for anything OCR cannot name (icons, images).
Hover over unnamed icons in the frontmost window and read their tooltips, writing the names back to the interface graph. This is how you label icons that have no visible text. A tooltip is a label that appears when you hover the pointer over an icon for a moment. Choto hovers over each unnamed icon in turn, waits for its tooltip to appear, reads it with OCR, and writes the text back as the icon's name. The pass stops early if the frontmost window changes, the pointer is moved to the failsafe corner, or the stop button is pressed. It returns a report of what was named, what showed no tooltip, and what was unreadable.
Search the interface graph for a piece of text and return the windows where it was seen, ranked by relevance. This is how you find something you saw before. You supply a query (e.g. 'invoice', 'Settings', 'error message'). Choto searches the graph for windows containing that text, ranks them by how well the query matches, and returns the top hits with routes to reach them from the current window. Each hit says where the text was found (in an element, an icon label, or a window title), how well it matched, and how to get there (the sequence of steps to take).
Submit a completed task and return a summary. This is a placeholder for integration with external task systems. You supply a task ID and optional metadata. Choto logs the submission and returns a confirmation.
mission_* tools (mission_start, mission_status, mission_mark, mission_finish, mission_replay) lack error handling guidance. No documentation of what happens if a mission ID is invalid, if an item is not found, or how to recover.
submit tool has minimal description (placeholder text). Does not explain what task systems it integrates with, what metadata is expected, or what success looks like.
execute_plan's complex step schema (action, target, path, modifiers, etc.) is described in prose but not formalized as a JSON Schema. LLMs must parse natural language to construct valid steps, inviting malformed requests.