Skip to content

mcp-interact

mcp-interact is a native macOS MCP server that gives AI agents full accessibility, input, and system control. Built in Swift on the Model Context Protocol SDK, it exposes 51 tools across 11 modules. It is the first Bus Commons open-source release.

mcp-interact implements AXI-INTERACT — the Agent eXperience Interface applied to desktop interaction. AXI is a set of design principles for building agent-native tools that minimize token waste, maximize action density, and give agents reliable feedback about their effects on the world.

Sources/
MCPInteract/ Entry point (main.swift)
MCPInteractKit/ MCP server, tool registry, dispatch
Accessibility/ AX tree inspection, search, caching, observer
Input/ Mouse, keyboard, scroll via CGEvent
Capture/ Screenshot, screen recording (ScreenCaptureKit)
Audio/ Device listing, text-to-speech
Hotkeys/ Hotkey manifest index and navigation
Session/ Single-agent session mutex, audit log
System/ App listing, display info, clipboard
Policy/ Element and app denylist
Windows/ CGWindowList queries
TOON/ Token-Oriented Object Notation encoder

MCPInteractKit is a library product. It can run standalone as a stdio MCP server or be embedded in another application (CUBEdesk) via Swift Package Manager. When embedded, the server listens on TCP port 9781 instead of stdio.

The dispatch layer routes all 51 tools through a single dispatch() function that wraps each response with TOON encoding, contextual help lines, and generation tracking for mutating operations.

mcp-interact supports two transports:

  • stdio — default, for MCP clients like Claude Code. JSON-RPC 2.0 over stdin/stdout.
  • TCP — for library embedding. MCPInteractServer.startOnPort(9781) creates an NWListener and wraps each accepted connection in NetworkTransport.

51 tools organized into 11 categories. 24 require an active session. All responses are TOON-encoded with contextual help lines.

ToolDescription
screenshotFull screen or region capture, JPEG scaled to 1280px max
screenshot_windowCapture a specific window by name or ID
get_screen_infoDisplay dimensions, Retina scale, cursor position
list_windows_toolAll visible windows with positions, sizes, PIDs
inspect_uiAX element tree for a running app (compact or full detail)
find_elementSearch AX tree for first element matching role/title/description
find_elementsSearch for all matching elements (max 100)
ax_focusedCurrently focused element across all apps
ax_menu_barFull menu bar structure for an app
ToolDescription
app_listRunning applications with bundle IDs and PIDs
app_frontmostCurrently frontmost application
app_activateBring an app to front or launch by bundle ID (session required)
clipboard_readClipboard contents: text, file URLs, image presence
system_stateBattery, thermal, displays, memory, OS version
ToolDescription
ax_watchStart observing an app for focus, value, and window events
ax_unwatchStop observing an app
ax_eventsDrain buffered AX events from watched apps
ToolDescription
ax_pressPress a button or activate a control via AX
ax_set_valueSet value on a text field, slider, or control
ax_focusSet keyboard focus to an element
ax_raiseBring an app and its main window to front via AX
ax_actionsList available actions and attributes for an element
ax_performPerform any AX action on an element
ax_cacheFind an element and cache it; returns a handle_id for reuse
ToolDescription
click_atClick at coordinates or on a named AX element
double_click_atDouble-click at coordinates
type_text_toolType text at current focus (handles Unicode)
keyPress a key combination (e.g. cmd+shift+n)
scroll_atScroll at a coordinate position
run_applescript_toolExecute AppleScript with denylist filtering
ToolDescription
acquire_sessionAcquire exclusive screen control (blocks until granted or timeout)
release_sessionRelease session, wake next agent in queue
extend_sessionExtend TTL of active session
session_statusCurrent holder, TTL remaining, queue state
ToolDescription
list_hotkeysQuery the hotkey index by app, kind, or search query
list_hotkey_appsList apps with registered hotkey manifests
hotkey_jumpNavigate to a surface by firing its hotkey chord (session required)
hotkey_rescanRe-scan filesystem for hotkey manifests
hotkey_registerRegister a hotkey manifest programmatically
ToolDescription
audio_devicesList audio input and output devices
speakText-to-speech via macOS TTS (session required)
voice_listAvailable TTS voices with language and gender
now_playingCurrent media: track, artist, album, elapsed time

Screen recording (session required, macOS 15+)

Section titled “Screen recording (session required, macOS 15+)”
ToolDescription
record_startStart recording screen or window to MP4
record_stopStop recording, return file path and duration
record_listList active recordings
ToolDescription
capabilitiesServer version, tool count, session state, hotkey index stats
ToolDescription
navigateHotkey jump + inspect UI in one call, with settle delay
interactFind element + perform action + return updated state
fillFind text field + focus + type + return confirmed value
activate_and_inspectBring app forward + return its AX tree

Compound tools collapse multi-step agent patterns into single MCP calls. Each includes a configurable settle_ms parameter so the UI can stabilize between the action and the perception step. Default settle times: navigate 300ms, interact 200ms, fill 100ms, activate_and_inspect 500ms.

AXI-INTERACT applies ten design principles that make every tool response optimized for agent consumption.

All responses are encoded in Token-Oriented Object Notation instead of JSON. TOON achieves 30-60% token savings through:

  • Unquoted keys and safe string values
  • Indentation-based nesting instead of braces
  • Tabular array encoding with field names in a header row
  • Inline primitive arrays with count prefix

JSON response:

{"server":"mcp-interact","version":"0.1.0","platform":"macOS",
"tool_count":51,"session":{"current_session":null,"queue_length":0,
"queue":[]},"hotkey_index":{"apps":0,"entries":0}}

TOON response:

server: mcp-interact
version: 0.1.0
platform: macOS
tool_count: 51
session:
current_session: null
queue_length: 0
queue: []
hotkey_index:
apps: 0
entries: 0

For tabular data (lists of windows, apps, hotkeys), TOON uses a header row with field names and compact inline rows:

windows[3]{name,pid,x,y,width,height}:
Finder,412,0,25,1728,1092
Safari,891,100,50,1200,800
Terminal,1023,200,100,800,600

inspect_ui defaults to compact mode, returning only role, title, value, and bounds per element. Full mode adds subrole, description, identifier, enabled, focused, and selected. In compact mode, position data is collapsed into a single bounds array [x, y, width, height].

Compact mode reduces per-element token count by approximately 60% in tree views. Compound tools (navigate, activate_and_inspect) use compact mode for embedded trees.

Every successful response includes a help[] line with 2-3 suggested next actions:

server: mcp-interact
tool_count: 51
...
help[acquire_session to start acting | list_hotkeys to see navigation shortcuts]

30 tools have help hints. The mapping is defined in ToolDispatch.swift and appended by the dispatch wrapper. Error responses do not include help lines.

Element handles use the format g{generation}:{counter} (e.g. g1:42). When a mutating action succeeds, the element cache generation advances. If an agent tries to use a handle from a previous generation, it gets a STALE_REF error with the current generation number and a recovery hint to re-inspect.

16 mutating tools advance the generation: ax_press, ax_set_value, ax_focus, ax_raise, ax_perform, click_at, double_click_at, type_text_tool, key, scroll_at, run_applescript_tool, app_activate, navigate, interact, fill, activate_and_inspect.

Every mutation response includes _gen: N so agents can track freshness.

Every error response includes a recovery: line with actionable guidance:

error: Missing required parameter: app_name
recovery: check the tool schema and provide the missing parameter
error: STALE_REF: handle g1:42 is from a previous generation (current: g2).
recovery: call inspect_ui or find_element to get fresh handles for the current UI state

Error categories and their recovery patterns:

CategoryRecovery
Missing parameterCheck the tool schema and provide it
Policy deniedChoose a different app or element
Stale referenceCall inspect_ui or find_element for fresh handles
Permission deniedGrant Accessibility access in System Settings
App not foundCall app_list to see running apps
Handle not foundCall find_element or ax_cache for a fresh handle
Session requiredCall acquire_session first
Unknown toolCall tools/list to see available tools

Perception tools return descriptive messages instead of bare false values:

found: false
empty: No element matching [button, Save] in Finder
found: false
empty: No element currently has keyboard focus

The four compound tools (navigate, interact, fill, activate_and_inspect) combine perception and action into single calls. Each performs a UI-settling wait between the action and the return read, eliminating the agent’s need to guess timing. This is the highest-value AXI optimization: it turns a 3-4 tool call sequence with sleep-and-retry into a single deterministic call.

AX action tools (ax_press, ax_set_value, ax_focus, ax_perform) return the updated element state after performing the action. The agent does not need to call find_element or ax_focused to verify its effect.

Every tool carries MCP annotations that inform the client about its behavior:

  • readOnlyHint — perception tools that do not change system state
  • destructiveHint — action tools that modify UI or input state
  • idempotentHint — tools whose output is stable across repeated calls

TOON (Token-Oriented Object Notation) is a text serialization format designed for LLM consumption. It optimizes for token count rather than parse speed.

Objects use key: value pairs with indentation-based nesting:

name: mcp-interact
version: 0.1.0
config:
max_clicks: 10
timeout: 60

Keys are unquoted if they match [A-Za-z_][A-Za-z0-9_.]*. Otherwise they are double-quoted.

String values are unquoted unless they:

  1. Are empty
  2. Have leading or trailing whitespace
  3. Are a reserved word (true, false, null)
  4. Look like a number
  5. Contain :, ", \, [, ], {, }, ,, or control characters
  6. Equal - or start with -

Quoted strings use JSON-style escaping (\\, \", \n, \r, \t, \uXXXX).

Numbers use shortest round-trippable representation. Trailing zeros are stripped. Integer-valued doubles render without a decimal point. NaN and Infinity become null. Negative zero becomes 0.

Booleans render as true / false. Correctly distinguished from NSNumber via CFBooleanGetTypeID().

Null renders as null.

Arrays have five representations:

  1. Empty[]
  2. Inline primitiveitems[3]: apple,banana,cherry
  3. Tabular (all dicts with identical keys, all-primitive values) — header row with field names, data rows with comma-separated values:
    users[2]{name,age,active}:
    alice,30,true
    bob,25,false
  4. Array of arrays- [count]: values per row
  5. Mixed- key: value list items with indented sub-fields

TOON saves tokens through six mechanisms:

MechanismJSONTOONSaving
No braces/brackets for objects{ } ,indentation3+ tokens per object
Unquoted keys"key"key2 tokens per key
Unquoted safe strings"value"value2 tokens per value
Tabular arraysrepeat field names per rowsingle headerN-1 repetitions saved
Inline primitives[1, 2, 3]items[3]: 1,2,3brackets + spaces
Count prefiximplicit from parsing[3]no overhead

Real-world measurements on mcp-interact tool responses show 30-60% token reduction depending on response structure, with tabular arrays achieving the highest savings.

Applications publish hotkey manifests so agents can navigate by semantic name instead of memorizing key chords.

{
"app": "Finder",
"app_version": "14.0",
"entries": [
{
"key": "finder.go.home",
"chord": "cmd+shift+h",
"label": "Home Folder",
"kind": "panel"
},
{
"key": "finder.file.new_folder",
"chord": "cmd+shift+n",
"label": "New Folder",
"kind": "action"
}
]
}

Each entry has four fields:

FieldDescription
keyUnique dotted identifier (e.g. finder.go.home)
chordKey combination string (e.g. cmd+shift+h)
labelHuman-readable display name
kindCategory: action, view, panel, modal, toggle

Manifests can be registered via the hotkey_register tool or placed as JSON files in ~/Library/Application Support/{AppName}/hotkeys.json for auto-scanning via hotkey_rescan.

mcp-interact uses a single-agent session mutex. Only one agent can hold screen control at a time.

Agent A → acquire_session(agent_id, purpose, ttl=120)
→ granted: true, session_id: "abc123", expires_in: 120
Agent B → acquire_session(agent_id, purpose, timeout=60)
→ blocks until A releases or timeout
Agent A → release_session(agent_id, session_id)
→ Agent B unblocked, granted session

TTL — sessions expire after ttl seconds (default 120) without renewal. extend_session resets the timer. An expiry watchdog runs on the session manager and automatically releases expired sessions.

Queue — when a session is held, new requests queue with a configurable timeout. If the timeout expires before the session becomes available, the request returns granted: false with a reason.

Action recording — every session-required tool call records an action timestamp via recordAction(), which is used for audit logging and activity tracking.

Session-scoped element cache — cached AX element handles are associated with a session. When a session ends, all its cached handles are cleared via clearSession().

Five layers protect the system from misuse:

Single-agent mutex — only one agent controls input at a time. No concurrent screen manipulation.

Rate limiting — configurable clicks-per-second cap (default 10). The rate limiter is checked before every click and double-click operation.

Audit logging — all actions are logged with agent ID, session ID, and detail metadata. Denied operations log the denial reason.

Policy denylist — element-level and app-level blocking. checkElementPolicy() inspects the serialized element info. checkFocusedAppPolicy() checks the frontmost app’s bundle ID. checkAppleScript() filters script content. All denylist checks happen before the action executes.

AppleScript filtering — AppleScript execution (run_applescript_tool) passes through the denylist before execution, blocking scripts that target restricted applications.

  • macOS 14+ (Sonoma)
  • Swift 5.10+
  • Accessibility permission (System Settings > Privacy & Security > Accessibility)
  • Screen Recording permission (for screenshot and recording tools)
Terminal window
swift build -c release
cp .build/release/mcp-interact /usr/local/bin/

Claude Code configuration:

Terminal window
claude mcp add mcp-interact -- /path/to/mcp-interact

Or in .mcp.json:

{
"mcpServers": {
"mcp-interact": {
"command": "/path/to/mcp-interact",
"args": []
}
}
}

Add to Package.swift:

dependencies: [
.package(path: "../mcp-interact"),
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "MCPInteractKit", package: "mcp-interact"),
]
),
]

Start the server on a TCP port:

import MCPInteractKit
let server = MCPInteractServer(name: "my-app-mcp", version: "0.1.0")
await server.configure()
try await server.startOnPort(9781)

The @_exported import MCP in MCPInteractServer.swift makes all MCP SDK types available transitively through MCPInteractKit.