MCP (Model Context Protocol) Support¶
Castella provides built-in support for MCP (Model Context Protocol), enabling AI agents to introspect and control Castella UIs programmatically.
Overview¶
MCP is a standard protocol for AI-UI interaction. With Castella's MCP support, AI agents can:
- Introspect the UI tree to understand what elements exist
- Read element properties (labels, values, states)
- Control widgets (click buttons, type text, toggle checkboxes)
- Integrate with A2UI for bidirectional AI-generated UIs
Installation¶
Install Castella with MCP support:
Quick Start¶
Basic MCP Server¶
from castella import App, Column, Button, Input, Text
from castella.frame import Frame
from castella.mcp import CastellaMCPServer
def build_ui():
return Column(
Text("Hello MCP!"),
Input("").semantic_id("name-input"),
Button("Submit").semantic_id("submit-btn"),
)
app = App(Frame("MCP Demo", 800, 600), build_ui())
mcp = CastellaMCPServer(app, name="my-app")
# Run SSE server for HTTP clients
mcp.run_sse_in_background(host="localhost", port=8765)
app.run()
Controlling from a Client¶
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 the input field
result = call_tool("type_text", element_id="name-input", text="Hello", replace=True)
print(f"Type result: {result}")
# Click the submit button
result = call_tool("click", element_id="submit-btn")
print(f"Click result: {result}")
Semantic IDs¶
Widgets can be assigned stable semantic IDs that persist across UI rebuilds:
# Assign semantic IDs using the fluent API
Button("Submit").semantic_id("submit-btn")
Input("").semantic_id("name-input")
CheckBox(checked_state).semantic_id("newsletter-opt-in")
# Without semantic IDs, auto-generated IDs are used:
# button_0, input_1, checkbox_2, etc.
Semantic IDs are crucial for reliable AI agent interaction - they provide stable identifiers that don't change when the UI is rebuilt.
Transport Options¶
SSE Transport (HTTP)¶
The SSE (Server-Sent Events) transport is ideal for connecting to already-running Castella applications:
mcp = CastellaMCPServer(app, name="my-app")
# Run SSE server on a specific port
mcp.run_sse_in_background(host="localhost", port=8765)
SSE Endpoints:
| Endpoint | Method | Description |
|---|---|---|
/sse |
GET | SSE event stream |
/message |
POST | Send MCP messages |
/health |
GET | Health check |
stdio Transport¶
For integration with Claude Desktop or other MCP clients that use stdio:
mcp = CastellaMCPServer(app, name="my-app")
# Run stdio server in background thread
mcp.run_in_background()
MCP Resources¶
Resources provide read-only access to UI state:
| URI | Description |
|---|---|
ui://tree |
Complete UI tree with semantic IDs and properties |
ui://focus |
Currently focused element |
ui://elements |
List of all interactive elements |
ui://element/{id} |
Detailed information about a specific element |
a2ui://surfaces |
A2UI surfaces (when A2UI renderer is provided) |
Reading Resources¶
def read_resource(uri: str) -> dict:
return send_message("read_resource", {"uri": uri})
# Get the complete UI tree
tree = read_resource("ui://tree")
# Get all interactive elements
elements = read_resource("ui://elements")
MCP Tools¶
Tools allow AI agents to interact with the UI:
| Tool | Parameters | Description |
|---|---|---|
click(element_id) |
element_id: str |
Click/tap an element |
type_text(element_id, text, replace) |
element_id: str, text: str, replace: bool = False |
Type into an input field |
focus(element_id) |
element_id: str |
Set focus to an element |
scroll(element_id, direction, amount) |
element_id: str, direction: str, amount: int = 100 |
Scroll (up/down/left/right) |
toggle(element_id) |
element_id: str |
Toggle a checkbox or switch |
select(element_id, value) |
element_id: str, value: str |
Select value in picker/radio/tabs |
list_actionable() |
(none) | List all interactive elements |
send_a2ui(message) |
message: dict |
Send A2UI message (when A2UI renderer provided) |
Tool Examples¶
# Click a button
call_tool("click", element_id="submit-btn")
# Type text (replace existing content)
call_tool("type_text", element_id="search-input", text="python", replace=True)
# Type text (append to existing)
call_tool("type_text", element_id="notes-input", text=" more text", replace=False)
# Toggle a checkbox
call_tool("toggle", element_id="agree-checkbox")
# Scroll down
call_tool("scroll", element_id="content-list", direction="down", amount=200)
# Select a tab
call_tool("select", element_id="main-tabs", value="settings")
A2UI + MCP Integration¶
Castella's MCP support integrates seamlessly with A2UI for bidirectional AI-UI interaction:
from castella import App
from castella.a2ui import A2UIRenderer, A2UIComponent, UserAction
from castella.frame import Frame
from castella.mcp import CastellaMCPServer
# A2UI JSON definition
A2UI_JSON = {
"components": [
{"id": "root", "component": "Column", "children": {"explicitList": ["input", "btn"]}},
{"id": "input", "component": "TextField", "text": {"path": "/name"}},
{"id": "btn", "component": "Button", "text": {"literalString": "Submit"},
"action": {"name": "submit", "context": []}},
],
"rootId": "root",
}
# Create A2UI renderer with action handler
def on_action(action: UserAction):
if action.name == "submit":
surface = renderer.get_surface("default")
name = surface.data_model.get("name", "")
print(f"Submitted: {name}")
renderer = A2UIRenderer(on_action=on_action)
renderer.render_json(A2UI_JSON, initial_data={"/name": ""})
surface = renderer.get_surface("default")
app = App(Frame("A2UI + MCP", 800, 600), A2UIComponent(surface))
# MCP server with A2UI renderer for bidirectional integration
mcp = CastellaMCPServer(app, a2ui_renderer=renderer)
mcp.run_sse_in_background(host="localhost", port=8766)
app.run()
Key Features:
- A2UI component IDs are automatically used as MCP semantic IDs
- MCP tools can control A2UI-generated widgets
- User actions are properly routed through A2UI's action system
- Data bindings are maintained across MCP operations
Architecture¶
┌─────────────────────────────────────────────────────────────┐
│ AI Agent (Claude, etc.) │
│ ↓↑ │
│ MCP Client │
└─────────────────────────────────────────────────────────────┘
↓↑ SSE/stdio (JSON-RPC)
┌─────────────────────────────────────────────────────────────┐
│ Castella Application │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CastellaMCPServer (background thread) │ │
│ │ ┌────────────────┐ ┌────────────────────────────┐ │ │
│ │ │ Resources │ │ Tools │ │ │
│ │ │ ui://tree │ │ click(), type_text() │ │ │
│ │ │ ui://focus │ │ scroll(), toggle() │ │ │
│ │ │ ui://elements │ │ send_a2ui() │ │ │
│ │ └────────────────┘ └────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓↑ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SemanticWidgetRegistry + WidgetIntrospector │ │
│ │ - Stable semantic ID management │ │
│ │ - A2UI ID integration │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓↑ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Widget Tree (main thread) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Examples¶
Run the included examples to see MCP in action:
# Basic MCP SSE server
uv run python examples/mcp_sse_server.py
# MCP SSE client (in another terminal)
uv run python examples/mcp_sse_client.py
# A2UI + MCP server
uv run python examples/mcp_a2ui_server.py
# A2UI + MCP client (in another terminal)
uv run python examples/mcp_a2ui_client.py
Module Reference¶
castella.mcp¶
| Class/Function | Description |
|---|---|
CastellaMCPServer |
Main MCP server class |
SemanticWidgetRegistry |
Widget ID registry |
WidgetIntrospector |
UI tree introspection |
ElementInfo |
Element information model |
UITreeNode |
Tree node model |
ActionResult |
Tool result model |
CastellaMCPServer API¶
class CastellaMCPServer:
def __init__(
self,
app: App,
name: str = "castella-ui",
a2ui_renderer: A2UIRenderer | None = None,
): ...
def run(self) -> None:
"""Run MCP server (blocking, stdio transport)."""
def run_in_background(self) -> threading.Thread:
"""Run MCP server in background thread (stdio transport)."""
def run_sse_in_background(
self,
host: str = "localhost",
port: int = 8765,
) -> threading.Thread:
"""Run SSE server in background thread."""
def refresh_registry(self) -> None:
"""Refresh the widget registry from the current UI tree."""
Best Practices¶
-
Use Semantic IDs - Always assign meaningful semantic IDs to interactive elements for reliable AI agent interaction.
-
Handle Actions Properly - When using A2UI integration, implement proper action handlers to respond to AI-initiated interactions.
-
Keep UI Simple - Complex UIs with many nested elements can be harder for AI agents to navigate. Consider flattening the structure where possible.
-
Test with Clients - Use the provided example clients to verify your MCP integration works correctly.
-
Error Handling - Check the
successfield in tool results and handle failures gracefully.