MCP as a Rendering Target

View Source

Most 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:

ComponentActions
Buttonclick
TextInputtype_into, clear, get_value
SelectListselect, get_selected, get_options
Checkboxtoggle, get_checked
Modalconfirm, dismiss
Tableselect_row, sort, get_rows
Treeexpand, 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 :hover focus 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 ~100

The 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)}
end

The 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()
end

The 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