Skip to main content
Glama
README.md
# 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

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

Or manually:
```bash
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](./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`:
```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`:
```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:
```json
{
  "mcpServers": {
    "atk-accessibility": {
      "command": "nix-shell",
      "args": ["--run", "python -m src"],
      "cwd": "/path/to/orca"
    }
  }
}
```

### From the command line (interactive test)

```bash
just shell
python -m src    # runs indefinitely on stdio
```

Pipe a raw MCP request to test individual tools:
```bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
  | python -m src
```

## Policy Engine

See [docs/policy.md](./policy.md) for the full reference.

Quick example — deny all `heading` nodes and redact `textbox` names:
```yaml
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](./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](./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. `FOCUSED` → `focused`, `CHECKED` → `checked`).

## Development

See [docs/contribute.md](./contribute.md) for the development guide.

```bash
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