MCP as a Rendering Target
View SourceMost frameworks bolt MCP on as a side channel. Raxol treats it as a first-class rendering target alongside terminal, browser, and SSH. The Component tree is the source of truth; MCP tools and resources are projections of it. See ADR-0012 for the design rationale.
Quick Start
mix mcp.server
This starts an MCP server on stdio, with tools auto-derived from your app's Component tree. Wire it into Claude Code or any MCP client.
# In your app
defmodule MyApp do
use Raxol.Core.Runtime.Application
# ... your normal init/update/view ...
end
# In an MCP client
session = Raxol.MCP.Test.start_session(MyApp)
session
|> type_into("search", "elixir")
|> click("submit")
|> assert_component("results", fn c -> c[:content] != nil end)The agent sees a structured Component tree, not a flat screenshot. It picks the action it wants from a typed schema.
Tool Derivation
Each interactive Component implements Raxol.MCP.ToolProvider. The protocol exposes semantic actions per Component:
| Component | Actions |
|---|---|
Button | click |
TextInput | type_into, clear, get_value |
SelectList | select, get_selected, get_options |
Checkbox | toggle, get_checked |
Modal | confirm, dismiss |
Table | select_row, sort, get_rows |
Tree | expand, collapse, select_node |
Add @mcp_exclude true to a Component's attrs to suppress tool derivation, useful for internal scaffolding Components that shouldn't show up in the agent's action menu.
Focus Lens
A Component tree with 50 Components generates 100+ tools. That's too many for an LLM to reason about. The focus lens filters to ~15 tools per interaction based on:
- Current focused Component
- Mouse hover (in
:hoverfocus mode) - Modal stack (modals shadow background Components)
- Recently interacted-with Components
tools =
Raxol.MCP.FocusLens.filter(all_tools,
mode: :focused,
focused_id: "search_input",
max_tools: 15
)
length(tools) # ~15, not ~100The lens is attention-aware: agents see what a human would see, not a flat dump of every possible action.
Resources
Model state is exposed as MCP resources via projections declared on the app:
defmodule MyApp do
use Raxol.Core.Runtime.Application
@mcp_resource "myapp://state/cart"
def project_cart(model), do: %{items: model.cart, total: cart_total(model)}
endThe MCP client can read myapp://state/cart to inspect what the agent is working with. Updates stream as diffs through Raxol.MCP.Diff, so the agent doesn't need to re-fetch the full state every turn.
Test Harness
Raxol.MCP.Test is a pipe-friendly test harness:
import Raxol.MCP.Test
import Raxol.MCP.Test.Assertions
test "submit flow" do
session = start_session(MyApp)
session
|> type_into("email", "user@example.com")
|> type_into("password", "secret")
|> click("submit")
|> assert_component("status", fn c -> c.content == "Logged in" end)
|> stop_session()
endThe harness goes through the same MCP transport as a real client, so what your tests exercise is what an agent will hit.
Context Tree
Raxol.MCP.ContextTree assembles a unified view of state from:
- TEA model
- Component tree (with focus lens applied)
- Active agents (
Raxol.Agent.Registry) - Swarm state (when distributed)
- Pending notifications
The tree is streamed as diffs over the MCP connection, so agents track changes incrementally rather than polling.
Property Tests
Raxol.MCP.ToolProvider is functor-law-tested: tool derivation commutes with Component composition. If you compose two Components, the derived tools are the same as the tools you'd get by deriving them separately and merging. This catches bugs where a wrapping Component would accidentally hide tools from a child.
What This Enables
The same TEA module the human uses, the agent uses too. Same source of truth, different projections. That's the pitch a framework can only make if MCP is a first-class rendering target rather than an afterthought.
See Also
- ADR-0012: design rationale
- Agent Framework: agents that consume MCP
- Symphony: orchestrator that exposes its own MCP surface