Phoenix LiveView WebMCP Example

March 3, 2026 · View on GitHub

A minimal Phoenix LiveView application demonstrating WebMCP integration with server-side state management.

What This Demonstrates

This example shows how to integrate WebMCP with Phoenix LiveView using JavaScript hooks:

  • WebMCP tools registered via LiveView hooks
  • Bidirectional communication: AI -> JavaScript -> LiveView -> Server
  • Real-time state synchronization
  • Server-side state management with client-side AI access

Quick Start

Prerequisites

  • Elixir 1.14+ and Erlang/OTP 25+
  • Node.js 18+
  • A WebMCP-compatible client (browser extension, AI agent, etc.)

Installation

# Install Elixir dependencies
mix deps.get

# Install Node.js dependencies and build assets
mix setup

# Start the development server
mix phx.server

Open http://localhost:4000 and connect your WebMCP client to discover the available tools.

Available Tools

This example exposes 6 AI-callable tools:

ToolDescription
increment_counterIncrease the counter by 1
decrement_counterDecrease the counter by 1 (minimum 0)
set_counterSet the counter to a specific value
add_itemAdd a new item to the list
remove_itemRemove an item by ID
get_stateGet current counter and items state

How It Works

Architecture

AI Agent / WebMCP Client
           |
           v
  navigator.modelContext (WebMCP API)
           |
           v
  LiveView Hook (JavaScript)
           |
           v (pushEvent)
  LiveView (Elixir)
           |
           v
     Server State

Key Components

1. LiveView Hook (assets/js/app.js)

The WebMCP hook registers tools and communicates with LiveView:

const WebMCPHook = {
  mounted() {
    // Register tool that calls LiveView
    navigator.modelContext.registerTool({
      name: "increment_counter",
      description: "Increase the counter by 1",
      inputSchema: { type: "object", properties: {} },
      async execute() {
        this.pushEvent("increment", {});
        return {
          content: [{ type: "text", text: "Counter incremented" }]
        };
      }
    });
  }
};

2. LiveView (lib/webmcp_demo_web/live/counter_live.ex)

Handles events from the hook and manages state:

def handle_event("increment", _params, socket) do
  new_count = socket.assigns.count + 1
  {:noreply, assign(socket, :count, new_count)}
end

3. Template

Connects the hook to the LiveView element:

<div id="webmcp-app" phx-hook="WebMCP">
  <!-- UI content -->
</div>

Project Structure

phoenix-liveview/
├── assets/
│   ├── js/
│   │   └── app.js           # WebMCP hook + LiveSocket setup
│   ├── css/
│   │   └── app.css          # Styles
│   ├── package.json         # JS dependencies (@mcp-b/global)
│   └── tailwind.config.js
├── config/                   # Phoenix configuration
├── lib/
│   ├── webmcp_demo/
│   │   └── application.ex   # OTP application
│   └── webmcp_demo_web/
│       ├── components/      # Phoenix components
│       ├── live/
│       │   └── counter_live.ex  # Main LiveView
│       ├── endpoint.ex
│       └── router.ex
├── mix.exs                   # Elixir dependencies
├── package.json              # npm scripts
└── README.md

Key Patterns

Registering Tools in Hooks

// In your LiveView hook
mounted() {
  const self = this;

  navigator.modelContext.registerTool({
    name: "my_tool",
    description: "What it does",
    inputSchema: {
      type: "object",
      properties: {
        param: { type: "string", description: "Parameter" }
      },
      required: ["param"]
    },
    async execute(args) {
      // Push event to LiveView
      self.pushEvent("my_event", { value: args.param });
      return {
        content: [{ type: "text", text: "Done" }]
      };
    }
  });
}

Getting Data Back from LiveView

Use pushEvent with a callback for request-response patterns:

async execute() {
  return new Promise((resolve) => {
    self.pushEvent("get_data", {}, (reply) => {
      resolve({
        content: [{ type: "text", text: JSON.stringify(reply) }]
      });
    });
  });
}

With the LiveView handler using {:reply, ...}:

def handle_event("get_data", _params, socket) do
  {:reply, {:ok, socket.assigns.data}, socket}
end

Tool Cleanup

Always clean up tools when the hook is destroyed:

const WebMCPHook = {
  cleanupFns: [],

  mounted() {
    const cleanup = navigator.modelContext.registerTool({...});
    this.cleanupFns.push(cleanup);
  },

  destroyed() {
    this.cleanupFns.forEach(fn => fn());
    this.cleanupFns = [];
  }
};

Comparison: Phoenix vs Other Frameworks

FeaturePhoenix LiveViewReactVanilla JS
State LocationServerClientClient
State SyncWebSocket (automatic)ManualManual
Tool RegistrationLiveView HookuseWebMCP hookDirect API
Real-time UpdatesBuilt-inManualManual
SEOExcellentRequires SSRN/A

Development

# Start development server with live reload
mix phx.server

# Or run inside IEx for debugging
iex -S mix phx.server

# Format code
mix format

# Check types (if using dialyzer)
mix dialyzer

Learn More

WebMCP

Phoenix LiveView

Model Context Protocol

License

MIT