textual-mcp-server
# 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
Scored across 12 tools
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.
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.
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.
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.