castella-mcp
Enable AI agents to introspect and control Castella UIs via MCP. Create MCP servers, expose UI resources, handle MCP tools, and use semantic IDs.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install i2y-castella-castella-mcp
Repository
Skill path: skills/castella-mcp
Enable AI agents to introspect and control Castella UIs via MCP. Create MCP servers, expose UI resources, handle MCP tools, and use semantic IDs.
Open repositoryBest for
Primary workflow: Analyze Data & AI.
Technical facets: Full Stack, Frontend, Data / AI, Integration.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: i2y.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install castella-mcp into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/i2y/castella before adding castella-mcp to shared team environments
- Use castella-mcp for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: castella-mcp
description: Enable AI agents to introspect and control Castella UIs via MCP. Create MCP servers, expose UI resources, handle MCP tools, and use semantic IDs.
---
# Castella MCP Integration
MCP (Model Context Protocol) enables AI agents to introspect and control Castella UIs programmatically. This provides a standard protocol for AI-UI interaction.
**When to use**: "enable MCP for Castella", "MCP server", "semantic ID", "MCP resources", "MCP tools", "SSE transport", "CastellaMCPServer", "control UI with MCP"
## Quick Start
Create an MCP-enabled Castella app:
```python
from castella import App, Column, Button, Input, Text
from castella.frame import Frame
from castella.mcp import CastellaMCPServer
# Build UI with semantic IDs
ui = Column(
Text("Hello MCP!").semantic_id("greeting"),
Input("").semantic_id("name-input"),
Button("Submit").semantic_id("submit-btn"),
)
app = App(Frame("MCP Demo", 800, 600), ui)
# Create MCP server
mcp = CastellaMCPServer(app, name="my-castella-app")
mcp.run_in_background() # Run MCP in background thread
app.run() # Run UI on main thread
```
## Installation
```bash
uv sync --extra mcp # MCP dependencies
```
## Semantic IDs
Assign stable, human-readable identifiers to widgets:
```python
Button("Submit").semantic_id("submit-btn")
Input("").semantic_id("email-input")
CheckBox(state).semantic_id("newsletter-checkbox")
Text("Status").semantic_id("status-text")
```
Auto-generated IDs (if not specified): `button_0`, `input_1`, etc.
### Best Practices for Semantic IDs
- Use descriptive names: `submit-form-btn`, not `btn1`
- Use kebab-case: `user-name-input`
- Include widget type: `email-input`, `save-btn`
- Match action/purpose: `login-btn`, `search-input`
## MCP Resources
Read-only data available to AI agents:
| URI | Description |
|-----|-------------|
| `ui://tree` | Complete UI tree structure |
| `ui://focus` | Currently focused element |
| `ui://elements` | All interactive elements |
| `ui://element/{id}` | Specific element details |
| `a2ui://surfaces` | A2UI surfaces (if A2UI enabled) |
### Example: UI Tree Resource
```json
{
"type": "tree",
"root": {
"id": "root",
"type": "Column",
"children": [
{"id": "greeting", "type": "Text", "value": "Hello MCP!"},
{"id": "name-input", "type": "Input", "value": "", "interactive": true},
{"id": "submit-btn", "type": "Button", "label": "Submit", "interactive": true}
]
}
}
```
## MCP Tools
Actions AI agents can perform:
| Tool | Description | Parameters |
|------|-------------|------------|
| `click` | Click/tap element | `element_id` |
| `type_text` | Type into input | `element_id`, `text`, `replace` |
| `focus` | Set focus | `element_id` |
| `scroll` | Scroll container | `element_id`, `direction`, `amount` |
| `toggle` | Toggle checkbox/switch | `element_id` |
| `select` | Select in picker/tabs | `element_id`, `value` |
| `list_actionable` | List interactive elements | - |
| `send_a2ui` | Send A2UI message | `message` |
### Tool Examples
```python
# Click a button
click(element_id="submit-btn")
# Type into input (replace existing text)
type_text(element_id="name-input", text="Alice", replace=True)
# Type into input (append)
type_text(element_id="name-input", text=" Smith", replace=False)
# Toggle checkbox
toggle(element_id="newsletter-checkbox")
# Select tab
select(element_id="main-tabs", value="settings")
# Scroll down
scroll(element_id="message-list", direction="down", amount=100)
```
## Transports
### stdio (Default)
For MCP clients that communicate via stdin/stdout:
```python
mcp = CastellaMCPServer(app, name="my-app")
mcp.run_in_background() # Uses stdio transport
```
### SSE (HTTP)
For HTTP-based MCP clients (Claude Desktop, web clients):
```python
mcp = CastellaMCPServer(app, name="my-app")
mcp.run_sse_in_background(host="localhost", port=8765)
```
SSE endpoints:
- `GET /sse` - SSE event stream
- `POST /message` - Send MCP messages
- `GET /health` - Health check
## Example: MCP Client (Python)
Control a Castella app via HTTP:
```python
import json
import urllib.request
def call_tool(name: str, **kwargs) -> dict:
message = {
"type": "call_tool",
"params": {"name": name, "arguments": kwargs}
}
data = json.dumps(message).encode("utf-8")
req = urllib.request.Request(
"http://localhost:8765/message",
data=data,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as response:
return json.loads(response.read())
# Type into input
call_tool("type_text", element_id="name-input", text="Alice", replace=True)
# Click button
call_tool("click", element_id="submit-btn")
# Toggle checkbox
call_tool("toggle", element_id="newsletter-checkbox")
# List all interactive elements
result = call_tool("list_actionable")
print(result)
```
## A2UI + MCP Integration
Combine A2UI rendering with MCP control:
```python
from castella.a2ui import A2UIRenderer, A2UIComponent
from castella.mcp import CastellaMCPServer
renderer = A2UIRenderer(on_action=on_action)
renderer.render_json(a2ui_json)
surface = renderer.get_surface("default")
app = App(Frame("A2UI + MCP", 800, 600), A2UIComponent(surface))
# MCP with A2UI renderer for bidirectional integration
mcp = CastellaMCPServer(app, a2ui_renderer=renderer)
mcp.run_sse_in_background(port=8766)
app.run()
```
A2UI component IDs automatically become MCP semantic IDs.
### send_a2ui Tool
When A2UI renderer is provided, the `send_a2ui` tool becomes available:
```python
send_a2ui(message={
"updateDataModel": {
"surfaceId": "default",
"data": {"/counter": 42}
}
})
```
## API Reference
### CastellaMCPServer
```python
from castella.mcp import CastellaMCPServer
mcp = CastellaMCPServer(
app=app, # Castella App instance
name="my-app", # MCP server name
version="1.0.0", # Version string
a2ui_renderer=None, # Optional A2UIRenderer
)
# Blocking methods
mcp.run() # Run stdio (blocks)
mcp.run_sse(host, port) # Run SSE (blocks)
# Background methods
mcp.run_in_background() # Run stdio in thread
mcp.run_sse_in_background(host, port) # Run SSE in thread
# Management
mcp.refresh_registry() # Refresh widget registry
mcp.stop() # Stop server
```
### ElementInfo
Information about a UI element:
```python
element = {
"id": "submit-btn",
"type": "Button",
"label": "Submit",
"value": None,
"bounds": {"x": 10, "y": 100, "width": 80, "height": 40},
"interactive": True,
"focused": False,
}
```
## Best Practices
1. **Use descriptive semantic IDs** for all interactive elements
2. **Refresh registry** after major UI changes: `mcp.refresh_registry()`
3. **Use SSE transport** for remote/HTTP clients
4. **Combine with A2UI** for full agent-UI integration
5. **Handle errors** in tool calls gracefully
## Reference
- `references/resources.md` - Complete resource URI reference
- `references/tools.md` - Complete tool reference
- `references/types.md` - ElementInfo, UITreeNode types
- `scripts/` - Executable examples (mcp_basic.py, mcp_sse.py)
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/resources.md
```markdown
# MCP Resources Reference
Read-only data exposed by CastellaMCPServer.
## ui://tree
Complete UI tree structure.
**Response:**
```json
{
"type": "tree",
"root": {
"id": "root",
"type": "Column",
"semantic_id": null,
"bounds": {"x": 0, "y": 0, "width": 800, "height": 600},
"children": [
{
"id": "greeting",
"type": "Text",
"semantic_id": "greeting",
"value": "Hello MCP!",
"bounds": {"x": 0, "y": 0, "width": 800, "height": 24}
},
{
"id": "name-input",
"type": "Input",
"semantic_id": "name-input",
"value": "",
"interactive": true,
"focused": false,
"bounds": {"x": 0, "y": 24, "width": 800, "height": 40}
}
]
}
}
```
## ui://focus
Currently focused element.
**Response (focused):**
```json
{
"type": "focus",
"element": {
"id": "name-input",
"type": "Input",
"semantic_id": "name-input",
"value": "Alice"
}
}
```
**Response (no focus):**
```json
{
"type": "focus",
"element": null
}
```
## ui://elements
List of all interactive elements.
**Response:**
```json
{
"type": "elements",
"elements": [
{
"id": "name-input",
"type": "Input",
"semantic_id": "name-input",
"value": "",
"interactive": true
},
{
"id": "submit-btn",
"type": "Button",
"semantic_id": "submit-btn",
"label": "Submit",
"interactive": true
},
{
"id": "newsletter-check",
"type": "CheckBox",
"semantic_id": "newsletter-check",
"value": false,
"interactive": true
}
]
}
```
## ui://element/{id}
Details for a specific element.
**Request:**
```
ui://element/submit-btn
```
**Response:**
```json
{
"type": "element",
"element": {
"id": "submit-btn",
"type": "Button",
"semantic_id": "submit-btn",
"label": "Submit",
"interactive": true,
"focused": false,
"enabled": true,
"visible": true,
"bounds": {
"x": 10,
"y": 100,
"width": 80,
"height": 40
}
}
}
```
**Error (not found):**
```json
{
"type": "error",
"message": "Element not found: unknown-id"
}
```
## a2ui://surfaces
A2UI surfaces (only available when A2UI renderer is provided).
**Response:**
```json
{
"type": "surfaces",
"surfaces": [
{
"id": "default",
"root_id": "root",
"component_count": 15,
"data_model": {
"/counter": 42,
"/user/name": "Alice"
}
}
]
}
```
## Element Properties
Common properties in element responses:
| Property | Type | Description |
|----------|------|-------------|
| `id` | string | Internal widget ID |
| `semantic_id` | string | User-assigned semantic ID |
| `type` | string | Widget type (Button, Input, etc.) |
| `value` | any | Current value |
| `label` | string | Display label (buttons) |
| `interactive` | bool | Can be interacted with |
| `focused` | bool | Currently has focus |
| `enabled` | bool | Not disabled |
| `visible` | bool | Currently visible |
| `bounds` | object | Position and size |
## Bounds Object
```json
{
"x": 10, // X position in pixels
"y": 100, // Y position in pixels
"width": 80, // Width in pixels
"height": 40 // Height in pixels
}
```
## Fetching Resources
Using MCP protocol:
```python
# Read resource
message = {
"type": "read_resource",
"params": {"uri": "ui://tree"}
}
# Send to MCP server and get response
response = send_mcp_message(message)
print(response["contents"])
```
```
### references/tools.md
```markdown
# MCP Tools Reference
Actions available via CastellaMCPServer.
## click
Click or tap an element.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `element_id` | string | Yes | Semantic ID of element |
**Example:**
```json
{
"type": "call_tool",
"params": {
"name": "click",
"arguments": {
"element_id": "submit-btn"
}
}
}
```
**Response:**
```json
{
"type": "tool_result",
"content": {"success": true, "element_id": "submit-btn"}
}
```
## type_text
Type text into an input field.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `element_id` | string | Yes | Semantic ID of input |
| `text` | string | Yes | Text to type |
| `replace` | bool | No | Replace existing text (default: false) |
**Example (replace):**
```json
{
"type": "call_tool",
"params": {
"name": "type_text",
"arguments": {
"element_id": "name-input",
"text": "Alice Smith",
"replace": true
}
}
}
```
**Example (append):**
```json
{
"type": "call_tool",
"params": {
"name": "type_text",
"arguments": {
"element_id": "name-input",
"text": " Jr.",
"replace": false
}
}
}
```
## focus
Set focus to an element.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `element_id` | string | Yes | Semantic ID of element |
**Example:**
```json
{
"type": "call_tool",
"params": {
"name": "focus",
"arguments": {
"element_id": "search-input"
}
}
}
```
## scroll
Scroll a container.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `element_id` | string | Yes | Semantic ID of scrollable |
| `direction` | string | Yes | "up", "down", "left", "right" |
| `amount` | number | No | Pixels to scroll (default: 100) |
**Example:**
```json
{
"type": "call_tool",
"params": {
"name": "scroll",
"arguments": {
"element_id": "message-list",
"direction": "down",
"amount": 200
}
}
}
```
## toggle
Toggle a checkbox or switch.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `element_id` | string | Yes | Semantic ID of checkbox/switch |
**Example:**
```json
{
"type": "call_tool",
"params": {
"name": "toggle",
"arguments": {
"element_id": "dark-mode-switch"
}
}
}
```
**Response:**
```json
{
"type": "tool_result",
"content": {"success": true, "new_value": true}
}
```
## select
Select a value in picker, radio buttons, or tabs.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `element_id` | string | Yes | Semantic ID of selector |
| `value` | string | Yes | Value to select |
**Example (tabs):**
```json
{
"type": "call_tool",
"params": {
"name": "select",
"arguments": {
"element_id": "main-tabs",
"value": "settings"
}
}
}
```
**Example (radio):**
```json
{
"type": "call_tool",
"params": {
"name": "select",
"arguments": {
"element_id": "size-picker",
"value": "Large"
}
}
}
```
## list_actionable
List all interactive elements.
**Parameters:** None
**Example:**
```json
{
"type": "call_tool",
"params": {
"name": "list_actionable",
"arguments": {}
}
}
```
**Response:**
```json
{
"type": "tool_result",
"content": {
"elements": [
{"id": "name-input", "type": "Input", "actions": ["type_text", "focus"]},
{"id": "submit-btn", "type": "Button", "actions": ["click"]},
{"id": "dark-mode", "type": "Switch", "actions": ["toggle"]},
{"id": "main-tabs", "type": "Tabs", "actions": ["select"]}
]
}
}
```
## send_a2ui
Send A2UI message (only available when A2UI renderer is provided).
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `message` | object | Yes | A2UI message |
**Example (update data):**
```json
{
"type": "call_tool",
"params": {
"name": "send_a2ui",
"arguments": {
"message": {
"updateDataModel": {
"surfaceId": "default",
"data": {
"/counter": 42,
"/status": "Updated!"
}
}
}
}
}
}
```
**Example (update components):**
```json
{
"type": "call_tool",
"params": {
"name": "send_a2ui",
"arguments": {
"message": {
"updateComponents": {
"surfaceId": "default",
"components": [
{"id": "status", "component": "Text", "text": {"literalString": "Done!"}}
]
}
}
}
}
}
```
## Error Responses
**Element not found:**
```json
{
"type": "tool_result",
"isError": true,
"content": {"error": "Element not found: unknown-id"}
}
```
**Invalid action:**
```json
{
"type": "tool_result",
"isError": true,
"content": {"error": "Element 'greeting' does not support 'click'"}
}
```
**Invalid parameters:**
```json
{
"type": "tool_result",
"isError": true,
"content": {"error": "Missing required parameter: element_id"}
}
```
```
### references/types.md
```markdown
# MCP Type Reference
Data types used in Castella MCP integration.
## ElementInfo
Information about a UI element.
```python
from castella.mcp import ElementInfo
element = ElementInfo(
id="submit-btn",
semantic_id="submit-btn",
widget_type="Button",
label="Submit",
value=None,
interactive=True,
focused=False,
enabled=True,
visible=True,
bounds=Rect(x=10, y=100, width=80, height=40),
)
```
### Properties
| Property | Type | Description |
|----------|------|-------------|
| `id` | str | Internal widget ID |
| `semantic_id` | str | User-assigned semantic ID |
| `widget_type` | str | Widget type name |
| `label` | str | Display label |
| `value` | Any | Current value |
| `interactive` | bool | Supports interaction |
| `focused` | bool | Has focus |
| `enabled` | bool | Not disabled |
| `visible` | bool | Currently visible |
| `bounds` | Rect | Position and size |
## UITreeNode
Node in the UI tree hierarchy.
```python
from castella.mcp import UITreeNode
node = UITreeNode(
id="root",
widget_type="Column",
semantic_id=None,
bounds=Rect(x=0, y=0, width=800, height=600),
children=[
UITreeNode(id="greeting", ...),
UITreeNode(id="input", ...),
],
)
```
### Properties
| Property | Type | Description |
|----------|------|-------------|
| `id` | str | Widget ID |
| `widget_type` | str | Widget type |
| `semantic_id` | str | Semantic ID |
| `bounds` | Rect | Position and size |
| `value` | Any | Current value |
| `label` | str | Display label |
| `interactive` | bool | Supports interaction |
| `focused` | bool | Has focus |
| `children` | list[UITreeNode] | Child nodes |
## ActionResult
Result from tool execution.
```python
from castella.mcp import ActionResult
# Success
result = ActionResult(
success=True,
element_id="submit-btn",
new_value=None,
)
# Error
result = ActionResult(
success=False,
error="Element not found: unknown-id",
)
```
### Properties
| Property | Type | Description |
|----------|------|-------------|
| `success` | bool | Action succeeded |
| `element_id` | str | Target element |
| `new_value` | Any | Updated value (for toggle) |
| `error` | str | Error message |
## SemanticWidgetRegistry
Maps semantic IDs to widgets.
```python
from castella.mcp import SemanticWidgetRegistry
registry = SemanticWidgetRegistry()
# Register widget
registry.register("submit-btn", button_widget)
# Lookup
widget = registry.get("submit-btn")
# List all
all_widgets = registry.all()
```
## WidgetIntrospector
Traverses UI tree and collects element info.
```python
from castella.mcp import WidgetIntrospector
introspector = WidgetIntrospector(app)
# Get tree
tree = introspector.get_tree()
# Get focused
focused = introspector.get_focused()
# Get all interactive
elements = introspector.get_interactive_elements()
# Get specific element
element = introspector.get_element("submit-btn")
```
## CastellaMCPServer
Main MCP server class.
```python
from castella.mcp import CastellaMCPServer
server = CastellaMCPServer(
app: App, # Castella App
name: str = "castella", # Server name
version: str = "1.0.0", # Version
a2ui_renderer: A2UIRenderer = None, # Optional A2UI
)
```
### Methods
| Method | Description |
|--------|-------------|
| `run()` | Run stdio transport (blocks) |
| `run_in_background()` | Run stdio in thread |
| `run_sse(host, port)` | Run SSE transport (blocks) |
| `run_sse_in_background(host, port)` | Run SSE in thread |
| `refresh_registry()` | Refresh widget registry |
| `stop()` | Stop server |
## Rect
Bounds rectangle.
```python
from castella.models.geometry import Rect, Point, Size
bounds = Rect(
origin=Point(x=10, y=100),
size=Size(width=80, height=40),
)
# Access
print(bounds.x) # 10
print(bounds.y) # 100
print(bounds.width) # 80
print(bounds.height) # 40
```
## JSON Serialization
All types serialize to JSON for MCP messages:
```python
element_dict = {
"id": "submit-btn",
"type": "Button",
"semantic_id": "submit-btn",
"label": "Submit",
"value": None,
"interactive": True,
"focused": False,
"enabled": True,
"visible": True,
"bounds": {
"x": 10,
"y": 100,
"width": 80,
"height": 40
}
}
```
```