Back to skills
SkillHub ClubAnalyze Data & AIFull StackFrontendData / AI

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.

Stars
39
Hot score
90
Updated
March 20, 2026
Overall rating
C2.1
Composite score
2.1
Best-practice grade
C65.6

Install command

npx @skill-hub/cli install i2y-castella-castella-mcp

Repository

i2y/castella

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 repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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

```

castella-mcp | SkillHub