Skip to main content
Glama
Nikitosina

keyboard-ext-ax-mcp

by Nikitosina

KeyboardExtAX

KeyboardExtAX accessibility snapshot illustration

Fast, read-only accessibility snapshots of active third-party iOS keyboard extensions in Simulator.

Why this project exists

A custom iOS keyboard runs in a separate app-extension process. When a client app presents that keyboard, ordinary UI automation usually sees the client application's accessibility tree but not the keyboard's useful descendants. The entire keyboard may appear as one opaque element.

That creates a practical automation gap:

  • screenshots can show the keyboard, but extracting reliable tap coordinates from pixels is slow and brittle;

  • the client application's accessibility snapshot does not necessarily expose the extension's keys and controls;

  • starting a new XCTest session for every query is expensive and can disturb keyboard focus;

  • XCTest element handles become stale whenever the keyboard changes layout;

  • automation agents should not need to manage test plans, ports, build products, or runner processes.

KeyboardExtAX fills that gap. It attaches XCTest directly to the already-running keyboard extension by bundle identifier, parses its accessibility hierarchy, and returns plain JSON with absolute logical coordinates. A persistent runner keeps warm snapshots fast, while a small controller owns building, caching, ports, and per-simulator lifecycle.

KeyboardExtAX observes only. Use a separate automation tool, such as XcodeBuildMCP, to launch apps, focus text fields, and perform gestures with the returned coordinates.

Related MCP server: ios-sim-mcp

Quick start: MCP

Configure keyboard-ext-ax-mcp as a stdio MCP server. Most MCP clients use a configuration shaped like this:

{
  "mcpServers": {
    "KeyboardExtAX": {
      "command": "keyboard-ext-ax-mcp",
      "env": {
        "DEVELOPER_DIR": "/Applications/Xcode.app/Contents/Developer"
      }
    }
  }
}

If the MCP client does not inherit your shell PATH, replace the command with the absolute path printed by command -v keyboard-ext-ax-mcp.

The server exposes one tool:

keyboard_snapshot(
  simulator_id: "<SIMULATOR_UDID>",
  extension_bundle_id: "com.example.keyboard.extension",
  raw: false
)

Example input:

{
  "simulator_id": "2D78332B-A3C3-4CEE-962B-99EF531FD201",
  "extension_bundle_id": "com.example.keyboard.extension"
}

The result is returned as structured MCP content. The MCP layer delegates directly to the same controller used by the CLI, so build caching, sessions, errors, and output are identical.

Quick start: CLI

With the client text field focused and the requested keyboard visible:

keyboard-ext-ax snapshot \
  --simulator <SIMULATOR_UDID> \
  --extension com.example.keyboard.extension

Add --output /tmp/keyboard.json to write the result to a file. Add --raw for the nested tree and XCTest diagnostics.

Scope and limitations

KeyboardExtAX currently supports:

  • macOS hosts;

  • iOS Simulator;

  • third-party keyboard extensions;

  • Xcode 26.5;

  • read-only accessibility snapshots.

It does not:

  • tap, swipe, or type;

  • launch or foreground the client app;

  • select or enable a keyboard for the user;

  • access physical iOS devices;

  • guarantee compatibility with widgets, Live Activities, or other extension types;

  • guarantee parser compatibility with untested Xcode versions.

The parser reads XCUIApplication.debugDescription. Its text format is not a documented compatibility contract, so each Xcode version must be validated before support is claimed.

Requirements

  • macOS

  • Xcode 26.5 with the desired iOS Simulator runtime installed

  • Python 3.10 or newer

  • A keyboard app and extension already installed in the target simulator

  • The keyboard enabled in Settings → General → Keyboard → Keyboards

  • A foreground client app with a focused text field

If multiple Xcode installations are present, select one with DEVELOPER_DIR or --developer-dir:

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer

Installation

brew install nikitosina/tap/keyboard-ext-ax

PyPI with uv

Install both executables permanently:

uv tool install keyboard-ext-ax

Or run the MCP server without a persistent installation:

uvx keyboard-ext-ax mcp

From source

git clone https://github.com/Nikitosina/KeyboardExtAX.git
cd KeyboardExtAX
python3 -m venv .venv
.venv/bin/python -m pip install -e .
source .venv/bin/activate

Every installation provides keyboard-ext-ax and keyboard-ext-ax-mcp. The MCP server can be started with either keyboard-ext-ax-mcp or keyboard-ext-ax mcp.

The bundled Xcode project is ready to use. XcodeGen is needed only when modifying keyboard_ext_ax/harness/project.yml.

Output

The default response is a compact, flat array:

{
  "ok": true,
  "snapshot_id": "9543E2AA-C4D9-4281-9645-7792F7953FE3",
  "simulator_udid": "2D78332B-A3C3-4CEE-962B-99EF531FD201",
  "extension_bundle_id": "com.example.keyboard.extension",
  "pid": 12345,
  "elapsed_ms": 103.4,
  "round_trip_ms": 106.1,
  "session_reused": true,
  "total_ms": 171.8,
  "elements": [
    {
      "ref": "0.0.3",
      "type": "Key",
      "identifier": "space",
      "label": "Space",
      "frame": {
        "x": 112,
        "y": 746,
        "width": 178,
        "height": 44
      },
      "center": {
        "x": 201,
        "y": 768
      },
      "tap_x": 201,
      "tap_y": 768
    }
  ]
}

Element fields

Field

Meaning

ref

Stable path within this snapshot only. Refresh it after layout changes.

type

XCTest accessibility element type.

identifier

Accessibility identifier, when provided by the extension.

label

Accessibility label, when provided by the extension.

frame

Absolute logical frame in Simulator coordinates.

center

Exact floating-point frame center.

tap_x, tap_y

Rounded integer center, ready for a gesture tool.

Coordinates are Simulator logical coordinates, not screenshot pixels. Do not rescale them before passing them to XcodeBuildMCP for the same simulator.

Session management

keyboard-ext-ax status
keyboard-ext-ax status --simulator <SIMULATOR_UDID>
keyboard-ext-ax stop --simulator <SIMULATOR_UDID>
keyboard-ext-ax stop --all

One runner is maintained per simulator. Multiple simulators can be queried concurrently. If a reused runner loses the extension process, KeyboardExtAX recycles it once and returns client_refocus_required.

Errors

Errors are JSON objects with stable codes.

Code

Meaning

Recovery

client_refocus_required

A new or restarted runner is ready.

Refocus the client text field and repeat the request.

extension_not_active

XCTest could not attach after three attempts.

Confirm that the requested extension—not the system keyboard—is visible, then retry.

runner_start_failed

The persistent XCTest runner did not become ready.

Inspect the returned log and log_tail.

build_failed

The XCTest harness failed to build.

Inspect the returned build log and verify the selected Xcode.

xcode_unavailable

Xcode could not be located or queried.

Set DEVELOPER_DIR or pass --developer-dir.

extension_bundle_id_missing

The runner received no extension identifier.

Supply extension_bundle_id.

snapshot_failed

XCTest raised an unexpected snapshot error.

Retry with raw: true and inspect diagnostics.

The CLI exits with:

  • 0 for success;

  • 2 for a normal snapshot-state error such as inactive extension;

  • 1 for lifecycle, build, or controller failures.

Architecture

MCP client / CLI
        │
        ▼
KeyboardExtAXController
  ├─ build cache
  ├─ per-simulator lock and state
  ├─ free-port allocation
  └─ XCTest runner lifecycle
        │  JSON over loopback TCP
        ▼
PersistentKeyboardExtAXTests
        │  XCUIApplication(bundleIdentifier:)
        ▼
Active keyboard extension process
        │
        ▼
Parsed tree → compact elements → MCP/CLI JSON

The host app included in this repository is XCTest scaffolding. Snapshot mode does not launch it and does not replace the consumer's foreground client app.

License

KeyboardExtAX is available under the MIT License.

Available Tools

1 tool
keyboard_snapshotA

Return the active keyboard extension accessibility tree and tap coordinates.

The client app must already be foreground with a focused text field presenting the requested extension. The first call builds and starts cached XCTest support; later calls reuse the simulator session.

ParametersJSON Schema
NameRequiredDescriptionDefault
rawNo
simulator_idYes
extension_bundle_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral disclosure burden. It does this well by noting that the first call builds and starts cached XCTest support while later calls reuse the simulator session, alerting the agent to an initialization cost and stateful behavior. It does not go into failure modes or permissions, but the core behavioral traits are disclosed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two short, purposeful sentences. The primary purpose is front-loaded, and the second sentence supplies only essential usage and behavioral context. No filler or redundant restatement of the tool name appears.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description provides the key prerequisite, the first-call initialization behavior, and the output type, with an output schema covering return value structure. The main gap is the lack of parameter documentation, but the parameter names and output schema give an agent enough to invoke the tool correctly in most cases.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, but it never mentions any of the three parameters. In particular, the 'raw' boolean flag has no explanation, and while simulator_id and extension_bundle_id are somewhat self-explanatory, their exact expected formats or relationship to the tool's behavior are left unspecified.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Return the active keyboard extension accessibility tree and tap coordinates.' This clearly distinguishes the tool's purpose from a generic snapshot or screenshot utility, even without sibling tools to compare against.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives an explicit precondition: the client app must already be foreground with a focused text field presenting the requested extension. It does not name alternatives or exclusions, but with no sibling tools listed, this level of context is sufficient for an agent to know when calling is appropriate.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.1.2
    • First observedkeyboard_snapshot

TDQS

A3.9/5.0

Scored across 1 tool

Disambiguation5/5

With only one tool, there is no possibility of confusing it with another. The tool's purpose is clearly stated in its name and description.

Naming Consistency4/5

The single tool name 'keyboard_snapshot' follows a clear noun_verb style and is descriptive. However, with only one tool there is no broader pattern to evaluate for consistency.

Tool Count2/5

A single tool for a keyboard extension accessibility server feels too thin for the apparent scope. Typical interaction would require additional actions like typing, tapping, or dismissing the keyboard.

Completeness2/5

The server provides only a snapshot/read operation with no corresponding actions or lifecycle coverage. Significant gaps exist for any real keyboard extension testing workflow.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to inspect and verify mobile UI hierarchies, nodes, styles, screenshots, and runtime attributes of iOS apps in simulators or on USB devices.
    261 npm
    2
    Apache 2.0
  • A
    license
    A
    quality
    B
    maintenance
    Enables driving an iOS Simulator from a model: taking screenshots, reading the UI tree, tapping, swiping, typing, managing app lifecycle and device state such as appearance, permissions, location, and notifications, without code signing or a physical device.
    21
    6 npm
    MIT