Skip to main content
Glama

Orca — ATK Accessibility MCP Server

Captures the ATK (Accessibility Toolkit) widget tree from running GTK applications on Linux, normalizes it to ARIA roles/types, applies a configurable declarative security policy, and exposes the result as MCP tools to any standard MCP client (Claude, Cursor, Windsurf, etc.).

Quick Start

cd orca
just shell        # enter nix-shell with all dependencies
just server       # start the MCP server on stdio

Or manually:

nix-shell
PYTHONPATH=src python3 -m src

Architecture

orca/
├── shell.nix                 # nix-shell environment
├── pyproject.toml            # package config
├── Justfile                  # task runner
├── docs/
│   ├── README.md             # this file
│   ├── usage.md              # client integration guide
│   ├── policy.md             # policy engine reference
│   ├── atk.md                # ATK capture internals
│   └── contribute.md         # development guide
└── src/
    ├── __init__.py
    ├── __main__.py           # entry point
    ├── atk.py                # ATK tree capture
    ├── normalize.py          # ATK→ARIA normalization
    ├── policy.py             # declarative security policy
    ├── server.py             # MCP server
    └── default_policy.yaml   # ship-default policy

MCP Tools

Tool

Params

Description

get_tree

none

Full ARIA-normalized tree, policy-filtered

get_tree_for_app

app_name: str

Tree scoped to an app (fnmatch glob)

get_node_info

node_id: str

Single node lookup by obj_id

list_apps

none

Top-level app objects (name, pid, role)

Configuration

Policy

Policy files are loaded in this priority:

  1. ~/.config/atk-mcp/policy.yaml (user override)

  2. src/default_policy.yaml (bundled default)

If neither exists or either fails to parse, the server starts with default_action: allow and no user rules.

Policy is loaded once at startup — restart the server to pick up changes.

See docs/policy.md for the full schema and examples.

Nix Environment

All dependencies are managed via shell.nix. No uv, no virtualenv. Key packages:

  • python314 — runtime

  • python314Packages.pyatspi — ATK tree access

  • python314Packages.pygobject3 — GI introspection

  • python314Packages.mcp — MCP SDK v2

  • python314Packages.pydantic-settings — policy config

  • python314Packages.pyyaml — policy parsing

  • at-spi2-core, at-spi2-atk, atk, gtk3 — runtime libs

Usage

With Cursor

Add to ~/.cursor/mcp.json:

{
  "mcpServers": {
    "atk-accessibility": {
      "command": "nix-shell",
      "args": ["--run", "python -m src"],
      "cwd": "/path/to/orca"
    }
  }
}

With Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "atk-accessibility": {
      "command": "nix-shell",
      "args": ["--run", "python -m src"],
      "cwd": "/path/to/orca"
    }
  }
}

With Windsurf

Add to .mcp.json in your project:

{
  "mcpServers": {
    "atk-accessibility": {
      "command": "nix-shell",
      "args": ["--run", "python -m src"],
      "cwd": "/path/to/orca"
    }
  }
}

From the command line (interactive test)

just shell
python -m src    # runs indefinitely on stdio

Pipe a raw MCP request to test individual tools:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
  | python -m src

Policy Engine

See docs/policy.md for the full reference.

Quick example — deny all heading nodes and redact textbox names:

default_action: allow
built_in_deny:
  aria_roles:
    - "password"
  state_keywords:
    - "hidden"
    - "invisible"
rules:
  - id: deny-headings
    conditions:
      role: "heading"
    action: deny
  - id: redact-forms
    conditions:
      role: "textbox"
    action: redact
    redact_fields:
      - "name"
      - "description"

ATK Capture

See docs/atk.md for internals. Key points:

  • Walks gi.repository.Atspi desktop root recursively

  • Fail-closed: subprocess isolation prevents GLib abort from crashing the server when no AT-SPI bus is available

  • Each node captures: obj_id, role (int), role_name, name, description, state_set, attributes, child_count, index_in_parent, app_name, pid

Normalization

See docs/normalize.md for the role map.

ATK integer roles (0–132) are mapped to ARIA role strings. Unmapped roles pass through as their role_name string. State names are translated (e.g. FOCUSEDfocused, CHECKEDchecked).

Development

See docs/contribute.md for the development guide.

just shell        # enter dev environment
just test         # run verification suite
just compile      # syntax check
just lint         # import + smoke check
just server       # start server for manual testing

Limitations

  • Wayland apps without AT-SPI: Some Wayland-native GTK apps don't expose AT-SPI interfaces. get_tree_for_app returns [] for those apps. Expected, not a bug.

  • No hot-reload: Policy is loaded once at startup.

  • Requires AT-SPI bus: Without a running accessibility bus (e.g. at-spi-bus-launcher), the ATK module returns [] gracefully.

  • Python 3.14+: No typing-extensions dependency.

License

MIT