orca
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
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues