Skip to main content
Glama
discohead

textual-mcp-server

by discohead
README.md
# textual-mcp-server

An [MCP](https://modelcontextprotocol.io) server that lets AI agents launch, interact with, and inspect [Textual](https://textual.textualize.io) TUI applications headlessly. Drive any Textual app through its full lifecycle — click buttons, type text, read widget state, take screenshots — all via the Model Context Protocol.

## Features

- **Headless app execution** — Launch any Textual app without a terminal, powered by Textual's built-in `Pilot` testing API
- **Full interaction toolkit** — Click, type, press keys, and hover over widgets using CSS selectors
- **Rich state inspection** — Snapshot the widget tree, query widgets by selector, and extract type-specific properties from 16+ widget types
- **Multi-session support** — Run multiple apps concurrently with isolated sessions
- **Error tracking** — Automatic collection of worker errors and app exceptions via message hooks
- **Screenshot capture** — Export the current screen as plain text or SVG

## Installation

Requires Python 3.10+.

```bash
pip install textual-mcp-server
```

For development:

```bash
git clone https://github.com/discohead/textual-mcp-server.git
cd textual-mcp-server
pip install -e ".[dev]"
```

## Quick Start

### As a standalone server

```bash
textual-mcp
```

### With Claude Code

```bash
claude mcp add textual -- textual-mcp
```

Or add manually to your MCP configuration (e.g., `~/.claude.json` or project `.mcp.json`):

```json
{
  "mcpServers": {
    "textual": {
      "command": "textual-mcp"
    }
  }
}
```

### Typical workflow

```
1. textual_launch("my_app.py")         → session_id
2. textual_snapshot(session_id)         → widget tree + focus + bindings
3. textual_click(session_id, "#submit") → interact
4. textual_screenshot(session_id)       → visual output
5. textual_stop(session_id)             → cleanup
```

## Tools

### Lifecycle

| Tool | Description |
|------|-------------|
| `textual_launch` | Launch a Textual app headlessly. Accepts a file path (`app.py`), path with class (`app.py:MyApp`), or module path (`mypackage.module:MyApp`). Returns a `session_id`. |
| `textual_stop` | Stop a running session and return any collected errors. |

### Interaction

| Tool | Description |
|------|-------------|
| `textual_press` | Simulate key presses (e.g., `["enter"]`, `["ctrl+s"]`). |
| `textual_click` | Click a widget by CSS selector with optional offset and repeat count. |
| `textual_type_text` | Type text into the focused input widget, with optional submit (Enter). |
| `textual_hover` | Hover the mouse over a widget by CSS selector. |

### Observation

| Tool | Description |
|------|-------------|
| `textual_snapshot` | Snapshot the widget tree with ref markers, focus state, active key bindings, and errors. |
| `textual_screenshot` | Capture the current screen as plain text or SVG. |
| `textual_query` | Query widgets matching a CSS selector. Returns type, ID, classes, and extracted properties. |
| `textual_get_screen_stack` | Get the current screen stack with modal indicators. |

### Assertion & Waiting

| Tool | Description |
|------|-------------|
| `textual_wait_for` | Wait for a condition: `idle`, `animation`, `workers` (all complete), or `widget` (selector appears). |
| `textual_check_errors` | Check for collected worker errors and app exceptions. |

## Architecture

```
textual_mcp/
├── server.py              # FastMCP server, tool registration, and tool implementations
├── session.py             # AppSession — headless app lifecycle via Pilot
├── session_manager.py     # Multi-session management
├── app_loader.py          # Dynamic app loading from file or module path
├── error_collector.py     # Message hook for worker error aggregation
└── serializers/
    ├── widget_tree.py     # DOM → indented text tree with [ref=N] markers
    └── widget_state.py    # Type-specific property extraction (16 widget types)
```

**Key design decisions:**

- **`AppSession`** wraps Textual's `App.run_test()` to provide launch/stop semantics with a persistent `Pilot` handle
- **`WidgetTreeSerializer`** produces LLM-friendly text output — interactive widgets get `[ref=N]` markers; scrollbars and hidden widgets are excluded
- **`WidgetStateExtractor`** uses an ordered isinstance registry to extract properties from Input, Button, DataTable, TextArea, Tree, and 11 other widget types
- **`ErrorCollector`** hooks into Textual's message system to capture `Worker.StateChanged` errors without disrupting normal operation

## Supported Widget Types

The state extractor provides rich property data for:

Input, Button, Static, Label, Checkbox, Switch, Select, TextArea, DataTable, Tree, ListView, OptionList, TabbedContent, ProgressBar, RadioSet, ContentSwitcher

## Development

```bash
# Run tests
pytest

# Run a specific test
pytest tests/test_integration_calculator.py -v
```

## Requirements

- Python >= 3.10
- [mcp](https://pypi.org/project/mcp/) >= 1.0.0
- [textual](https://pypi.org/project/textual/) >= 1.0.0

## License

MIT

TDQS

A3.5/5.0

Scored across 12 tools

Disambiguation5/5

Each tool targets a distinct action: launching, clicking, hovering, typing, pressing keys, querying widgets, taking screenshots, capturing state, waiting for conditions, checking errors, and stopping the app. No two tools overlap in purpose.

Naming Consistency5/5

All tools follow a consistent 'textual_' prefix followed by a descriptive verb in snake_case (e.g., textual_click, textual_query). No deviations in naming convention.

Tool Count5/5

With 12 tools, the server covers all essential interactions for headless QA of Textual apps: launching, UI manipulation, state inspection, and error checking. The count is well-scoped without unnecessary redundancy.

Completeness4/5

The tool set covers core workflows (launch, interact, query, debug, stop). Minor gaps like drag-and-drop or scroll are absent but not critical for typical testing scenarios.

Maintenance

ActivityInactive
ResponsivenessNo issues