diff --git a/AGENTS.md b/AGENTS.md
index cd5bd26..6650144 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -30,6 +30,14 @@ Welcome! This document helps you navigate the WebMCP Examples repository efficie
- **Key file**: `rails/app/javascript/controllers/bookmarks_webmcp_controller.ts` - Stimulus controller with WebMCP tools
- **API used**: `navigator.modelContext.registerTool()` in Stimulus
+#### Phoenix LiveView (Elixir)
+- **[phoenix-liveview/README.md](./phoenix-liveview/README.md)** - Counter + items with server-side state
+- **Location**: `/phoenix-liveview`
+- **Key files**:
+ - `lib/webmcp_demo_web/live/counter_live.ex` - LiveView with state management
+ - `assets/js/app.js` - WebMCP hook registration
+- **API used**: `navigator.modelContext.registerTool()` via LiveView hooks
+
### Legacy Examples (Deprecated - DO NOT USE)
- **[relegated/README.md](./relegated/README.md)** - Old examples using deprecated MCP SDK
- **Warning**: These use the legacy `@modelcontextprotocol/sdk` API
@@ -46,13 +54,17 @@ Welcome! This document helps you navigate the WebMCP Examples repository efficie
```bash
# Navigate to the example
-cd vanilla # or react, rails
+cd vanilla # or react, rails, phoenix-liveview
# Install dependencies
-pnpm install
+pnpm install # For JS examples
+# OR
+mix setup # For Phoenix example
# Start development server
-pnpm dev
+pnpm dev # For JS examples
+# OR
+mix phx.server # For Phoenix example
```
### Adding a New Example
@@ -164,6 +176,12 @@ example-name/
- Controller: `rails/app/javascript/controllers/bookmarks_webmcp_controller.ts`
- Config: `rails/vite.config.ts`
+**Phoenix LiveView Example:**
+- Entry: `phoenix-liveview/lib/webmcp_demo/application.ex`
+- LiveView: `phoenix-liveview/lib/webmcp_demo_web/live/counter_live.ex`
+- WebMCP Hook: `phoenix-liveview/assets/js/app.js`
+- Config: `phoenix-liveview/config/config.exs`
+
## WebMCP Package Documentation
- **[@mcp-b/global](https://docs.mcp-b.ai/packages/global)** - Core WebMCP polyfill for vanilla JS
@@ -190,7 +208,8 @@ example-name/
## Prerequisites
- **Node.js**: 18 or higher (see `.nvmrc`)
-- **pnpm**: Package manager
+- **pnpm**: Package manager (for JS examples)
+- **Elixir**: 1.14+ (for Phoenix example)
- **MCP-B Extension**: Chrome extension for testing WebMCP tools
- **Browser**: Chrome or Chromium-based browser
diff --git a/README.md b/README.md
index 395aa9b..526ba6c 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ git clone https://github.com/WebMCP-org/examples.git
cd examples
# Choose an example
-cd vanilla # or react, rails
+cd vanilla # or react, rails, phoenix-liveview
# Install and run
pnpm install
@@ -107,6 +107,24 @@ A bookmarks management application demonstrating Rails 7+ integration with Stimu
---
+### Phoenix LiveView Example
+
+**Location:** `/phoenix-liveview`
+
+A counter and item management app demonstrating WebMCP integration with Phoenix LiveView and server-side state.
+
+**Features:**
+- Uses `navigator.modelContext.registerTool()` via LiveView hooks
+- Server-side state management with real-time sync
+- Bidirectional communication: AI -> JavaScript -> LiveView -> Server
+- 6 AI-callable tools (counter operations + item CRUD + state query)
+
+**Tech:** Elixir, Phoenix 1.7, LiveView 1.0, `@mcp-b/global`
+
+[→ Documentation](./phoenix-liveview/README.md)
+
+---
+
### Legacy Examples (Deprecated)
**Location:** `/relegated`
@@ -223,7 +241,7 @@ WebMCP enables AI assistants to interact with websites through APIs instead of s
```bash
# Development (per example)
-cd vanilla # or react, rails
+cd vanilla # or react, rails, phoenix-liveview
pnpm dev # Run development server
pnpm build # Build for production
pnpm preview # Preview production build
@@ -243,15 +261,17 @@ pnpm preview # Preview production build
- [Vanilla Example](./vanilla/README.md) - Vanilla JavaScript implementation
- [React Example](./react/README.md) - React with hooks implementation
- [Rails Example](./rails/README.md) - Rails with Stimulus controllers
+- [Phoenix LiveView Example](./phoenix-liveview/README.md) - Elixir/Phoenix implementation
- [Legacy Examples](./relegated/README.md) - Deprecated implementations
## Tech Stack
-- **Package Manager:** pnpm
-- **Build Tool:** Vite 6
-- **Language:** TypeScript 5.6
+- **Package Manager:** pnpm (JS), Mix (Elixir)
+- **Build Tool:** Vite 6, esbuild
+- **Languages:** TypeScript 5.6, Elixir 1.14+
- **WebMCP Core:** @mcp-b/global
- **React Integration:** @mcp-b/react-webmcp
+- **Phoenix Integration:** LiveView hooks + @mcp-b/global
- **Validation:** JSON Schema, Zod
## Contributing
diff --git a/phoenix-liveview/.formatter.exs b/phoenix-liveview/.formatter.exs
new file mode 100644
index 0000000..e945e12
--- /dev/null
+++ b/phoenix-liveview/.formatter.exs
@@ -0,0 +1,5 @@
+[
+ import_deps: [:phoenix],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"]
+]
diff --git a/phoenix-liveview/.gitignore b/phoenix-liveview/.gitignore
new file mode 100644
index 0000000..73a161e
--- /dev/null
+++ b/phoenix-liveview/.gitignore
@@ -0,0 +1,27 @@
+# Elixir/Phoenix
+/_build/
+/deps/
+/.fetch
+erl_crash.dump
+*.ez
+*.beam
+/config/*.secret.exs
+.elixir_ls/
+
+# Assets
+/assets/node_modules/
+/priv/static/assets/
+
+# Generated
+/priv/static/cache_manifest.json
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
diff --git a/phoenix-liveview/.nvmrc b/phoenix-liveview/.nvmrc
new file mode 100644
index 0000000..3c03207
--- /dev/null
+++ b/phoenix-liveview/.nvmrc
@@ -0,0 +1 @@
+18
diff --git a/phoenix-liveview/README.md b/phoenix-liveview/README.md
new file mode 100644
index 0000000..67e9c4c
--- /dev/null
+++ b/phoenix-liveview/README.md
@@ -0,0 +1,260 @@
+# Phoenix LiveView WebMCP Example
+
+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
+
+```bash
+# 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](http://localhost:4000) and connect your WebMCP client to discover the available tools.
+
+## Available Tools
+
+This example exposes 6 AI-callable tools:
+
+| Tool | Description |
+|------|-------------|
+| `increment_counter` | Increase the counter by 1 |
+| `decrement_counter` | Decrease the counter by 1 (minimum 0) |
+| `set_counter` | Set the counter to a specific value |
+| `add_item` | Add a new item to the list |
+| `remove_item` | Remove an item by ID |
+| `get_state` | Get 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:
+
+```javascript
+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:
+
+```elixir
+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:
+
+```heex
+
+
+
+```
+
+## 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
+
+```javascript
+// 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:
+
+```javascript
+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, ...}`:
+
+```elixir
+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:
+
+```javascript
+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
+
+| Feature | Phoenix LiveView | React | Vanilla JS |
+|---------|-----------------|-------|------------|
+| State Location | Server | Client | Client |
+| State Sync | WebSocket (automatic) | Manual | Manual |
+| Tool Registration | LiveView Hook | useWebMCP hook | Direct API |
+| Real-time Updates | Built-in | Manual | Manual |
+| SEO | Excellent | Requires SSR | N/A |
+
+## Development
+
+```bash
+# 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
+- [WebMCP Documentation](https://docs.mcp-b.ai)
+- [WebMCP Quick Start](https://docs.mcp-b.ai/quickstart)
+- [@mcp-b/global Package](https://docs.mcp-b.ai/packages/global)
+
+### Phoenix LiveView
+- [Phoenix LiveView Docs](https://hexdocs.pm/phoenix_live_view)
+- [LiveView JavaScript Interop](https://hexdocs.pm/phoenix_live_view/js-interop.html)
+- [Phoenix Framework](https://phoenixframework.org)
+
+### Model Context Protocol
+- [MCP Documentation](https://modelcontextprotocol.io/)
+- [MCP Specification](https://spec.modelcontextprotocol.io/)
+
+## License
+
+MIT
diff --git a/phoenix-liveview/assets/css/app.css b/phoenix-liveview/assets/css/app.css
new file mode 100644
index 0000000..904754a
--- /dev/null
+++ b/phoenix-liveview/assets/css/app.css
@@ -0,0 +1,338 @@
+@import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities";
+
+/* Base styles */
+:root {
+ --color-primary: #4f46e5;
+ --color-primary-dark: #4338ca;
+ --color-success: #10b981;
+ --color-error: #ef4444;
+ --color-warning: #f59e0b;
+ --color-bg: #0f172a;
+ --color-surface: #1e293b;
+ --color-text: #f8fafc;
+ --color-text-muted: #94a3b8;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: system-ui, -apple-system, sans-serif;
+ background: var(--color-bg);
+ color: var(--color-text);
+ line-height: 1.6;
+ min-height: 100vh;
+}
+
+/* App container */
+.app {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+/* Header */
+.header {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+.header h1 {
+ font-size: 2.5rem;
+ font-weight: 700;
+ background: linear-gradient(135deg, #818cf8, #c084fc);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ margin-bottom: 0.5rem;
+}
+
+.subtitle {
+ color: var(--color-text-muted);
+ font-size: 1.1rem;
+}
+
+/* Notifications */
+.notifications {
+ position: fixed;
+ top: 1rem;
+ right: 1rem;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.notification {
+ padding: 0.75rem 1rem;
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ min-width: 250px;
+ animation: slideIn 0.3s ease-out;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+.notification-success {
+ background: var(--color-success);
+ color: white;
+}
+
+.notification-error {
+ background: var(--color-error);
+ color: white;
+}
+
+.notification-info {
+ background: var(--color-primary);
+ color: white;
+}
+
+.notification-warning {
+ background: var(--color-warning);
+ color: black;
+}
+
+.dismiss-btn {
+ background: transparent;
+ border: none;
+ color: inherit;
+ font-size: 1.25rem;
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.2s;
+}
+
+.dismiss-btn:hover {
+ opacity: 1;
+}
+
+/* Content layout */
+.content {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+/* Info section */
+.info-section {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+.info-card,
+.tools-card {
+ background: var(--color-surface);
+ border-radius: 1rem;
+ padding: 1.5rem;
+}
+
+.info-card h2,
+.tools-card h2 {
+ font-size: 1.25rem;
+ margin-bottom: 1rem;
+ color: var(--color-text);
+}
+
+.info-card ul,
+.tools-card ul {
+ list-style: none;
+ padding-left: 0;
+}
+
+.info-card li,
+.tools-card li {
+ padding: 0.5rem 0;
+ color: var(--color-text-muted);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.info-card li::before {
+ content: ">";
+ color: var(--color-primary);
+}
+
+.tools-card code {
+ background: rgba(79, 70, 229, 0.2);
+ color: #a5b4fc;
+ padding: 0.2rem 0.5rem;
+ border-radius: 0.25rem;
+ font-size: 0.875rem;
+}
+
+/* Cards */
+.card {
+ background: var(--color-surface);
+ border-radius: 1rem;
+ padding: 1.5rem;
+}
+
+.card h2 {
+ font-size: 1.25rem;
+ margin-bottom: 1rem;
+}
+
+/* Counter section */
+.counter-section {
+ display: flex;
+ justify-content: center;
+}
+
+.counter-card {
+ text-align: center;
+ min-width: 300px;
+}
+
+.counter-display {
+ margin: 2rem 0;
+}
+
+.count {
+ font-size: 5rem;
+ font-weight: 700;
+ background: linear-gradient(135deg, #818cf8, #c084fc);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.counter-controls {
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+}
+
+.last-action {
+ margin-top: 1rem;
+ color: var(--color-text-muted);
+ font-size: 0.875rem;
+}
+
+/* Items section */
+.items-card {
+ max-width: 600px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.empty-state {
+ text-align: center;
+ color: var(--color-text-muted);
+ padding: 2rem;
+}
+
+.items-list {
+ list-style: none;
+ margin-bottom: 1rem;
+}
+
+.item {
+ display: flex;
+ align-items: center;
+ padding: 0.75rem;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.item-name {
+ flex: 1;
+ font-weight: 500;
+}
+
+.item-id {
+ color: var(--color-text-muted);
+ font-size: 0.75rem;
+ margin-right: 1rem;
+}
+
+/* Buttons */
+.btn {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-primary {
+ background: var(--color-primary);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: var(--color-primary-dark);
+}
+
+.btn-secondary {
+ background: rgba(255, 255, 255, 0.1);
+ color: var(--color-text);
+}
+
+.btn-secondary:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.btn-danger {
+ background: var(--color-error);
+ color: white;
+}
+
+.btn-danger:hover {
+ background: #dc2626;
+}
+
+.btn-small {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+}
+
+/* Footer */
+.footer {
+ text-align: center;
+ margin-top: 3rem;
+ padding-top: 2rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ color: var(--color-text-muted);
+}
+
+.footer a {
+ color: var(--color-primary);
+ text-decoration: none;
+}
+
+.footer a:hover {
+ text-decoration: underline;
+}
+
+/* Phoenix LiveView specific */
+.phx-click-loading {
+ opacity: 0.5;
+ pointer-events: none;
+}
+
+.phx-modal {
+ opacity: 1 !important;
+}
diff --git a/phoenix-liveview/assets/js/app.js b/phoenix-liveview/assets/js/app.js
new file mode 100644
index 0000000..81a1c2b
--- /dev/null
+++ b/phoenix-liveview/assets/js/app.js
@@ -0,0 +1,164 @@
+/**
+ * Phoenix LiveView + WebMCP Integration
+ *
+ * Demonstrates integrating WebMCP with Phoenix LiveView using JavaScript hooks.
+ * Tools registered here expose server-side LiveView state to AI agents.
+ *
+ * @see https://docs.mcp-b.ai/packages/global - WebMCP global polyfill
+ * @see https://hexdocs.pm/phoenix_live_view/js-interop.html - LiveView JS interop
+ */
+
+import "@mcp-b/global";
+import { Socket } from "phoenix";
+import { LiveSocket } from "phoenix_live_view";
+
+/**
+ * WebMCP Hook for Phoenix LiveView
+ *
+ * Registers WebMCP tools on mount, cleans up on destroy.
+ * Each tool uses `this.pushEvent()` to communicate with the LiveView process.
+ */
+const WebMCPHook = {
+ /** @type {Array<() => void>} Cleanup functions for registered tools */
+ cleanupFns: [],
+
+ mounted() {
+ this.registerTools();
+ },
+
+ destroyed() {
+ this.cleanupFns.forEach((fn) => fn());
+ this.cleanupFns = [];
+ },
+
+ /**
+ * Register all WebMCP tools.
+ * Each tool calls `this.pushEvent()` to send events to LiveView.
+ */
+ registerTools() {
+ const hook = this;
+
+ // Counter tools
+ this.registerTool({
+ name: "increment_counter",
+ description: "Increase the counter by 1",
+ inputSchema: { type: "object", properties: {} },
+ async execute() {
+ hook.pushEvent("increment", {});
+ return { content: [{ type: "text", text: "Counter incremented" }] };
+ },
+ });
+
+ this.registerTool({
+ name: "decrement_counter",
+ description: "Decrease the counter by 1 (minimum 0)",
+ inputSchema: { type: "object", properties: {} },
+ async execute() {
+ hook.pushEvent("decrement", {});
+ return { content: [{ type: "text", text: "Counter decremented" }] };
+ },
+ });
+
+ this.registerTool({
+ name: "set_counter",
+ description: "Set the counter to a specific value",
+ inputSchema: {
+ type: "object",
+ properties: {
+ value: { type: "number", description: "Value to set (must be >= 0)" },
+ },
+ required: ["value"],
+ },
+ async execute({ value }) {
+ if (value < 0) {
+ return { content: [{ type: "text", text: "Error: Value must be >= 0" }] };
+ }
+ hook.pushEvent("set_count", { value });
+ return { content: [{ type: "text", text: `Counter set to ${value}` }] };
+ },
+ });
+
+ // Item management tools
+ this.registerTool({
+ name: "add_item",
+ description: "Add a new item to the list",
+ inputSchema: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "Name of the item to add" },
+ },
+ required: ["name"],
+ },
+ async execute({ name }) {
+ if (!name?.trim()) {
+ return { content: [{ type: "text", text: "Error: Name required" }] };
+ }
+ hook.pushEvent("add_item", { name: name.trim() });
+ return { content: [{ type: "text", text: `Added: ${name}` }] };
+ },
+ });
+
+ this.registerTool({
+ name: "remove_item",
+ description: "Remove an item from the list by its ID",
+ inputSchema: {
+ type: "object",
+ properties: {
+ id: { type: "number", description: "ID of the item to remove" },
+ },
+ required: ["id"],
+ },
+ async execute({ id }) {
+ hook.pushEvent("remove_item", { id });
+ return { content: [{ type: "text", text: `Removed item ${id}` }] };
+ },
+ });
+
+ // State query tool (uses reply callback for synchronous response)
+ this.registerTool({
+ name: "get_state",
+ description: "Get current counter value and all items",
+ inputSchema: { type: "object", properties: {} },
+ async execute() {
+ return new Promise((resolve) => {
+ hook.pushEvent("get_state", {}, (reply) => {
+ if (reply?.count !== undefined) {
+ const { count, items, item_count, last_action } = reply;
+ const itemList = items.length
+ ? items.map((i) => ` - ${i.name} (ID: ${i.id})`).join("\n")
+ : " (none)";
+ resolve({
+ content: [{
+ type: "text",
+ text: `Counter: ${count}\nItems (${item_count}):\n${itemList}\nLast action: ${last_action || "none"}`,
+ }],
+ });
+ } else {
+ resolve({ content: [{ type: "text", text: "Error fetching state" }] });
+ }
+ });
+ });
+ },
+ });
+ },
+
+ /** Register a tool and track cleanup function */
+ registerTool(config) {
+ const cleanup = navigator.modelContext.registerTool(config);
+ if (cleanup) this.cleanupFns.push(cleanup);
+ },
+};
+
+// Hooks registry - add WebMCP hook for LiveView elements with phx-hook="WebMCP"
+const Hooks = { WebMCP: WebMCPHook };
+
+// Initialize Phoenix LiveSocket with WebMCP hooks
+const csrfToken = document.querySelector("meta[name='csrf-token']").content;
+const liveSocket = new LiveSocket("/live", Socket, {
+ longPollFallbackMs: 2500,
+ params: { _csrf_token: csrfToken },
+ hooks: Hooks,
+});
+
+liveSocket.connect();
+window.liveSocket = liveSocket;
diff --git a/phoenix-liveview/assets/package.json b/phoenix-liveview/assets/package.json
new file mode 100644
index 0000000..59c6b07
--- /dev/null
+++ b/phoenix-liveview/assets/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "phoenix-liveview-webmcp-assets",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@mcp-b/global": "latest"
+ }
+}
diff --git a/phoenix-liveview/assets/tailwind.config.js b/phoenix-liveview/assets/tailwind.config.js
new file mode 100644
index 0000000..5b4af98
--- /dev/null
+++ b/phoenix-liveview/assets/tailwind.config.js
@@ -0,0 +1,37 @@
+// Tailwind CSS configuration for Phoenix LiveView + WebMCP demo
+
+const plugin = require("tailwindcss/plugin");
+const fs = require("fs");
+const path = require("path");
+
+module.exports = {
+ content: ["./js/**/*.js", "../lib/webmcp_demo_web/**/*.*ex"],
+ theme: {
+ extend: {
+ colors: {
+ brand: "#4f46e5",
+ },
+ },
+ },
+ plugins: [
+ require("@tailwindcss/forms"),
+ plugin(({ addVariant }) =>
+ addVariant("phx-click-loading", [
+ ".phx-click-loading&",
+ ".phx-click-loading &",
+ ])
+ ),
+ plugin(({ addVariant }) =>
+ addVariant("phx-submit-loading", [
+ ".phx-submit-loading&",
+ ".phx-submit-loading &",
+ ])
+ ),
+ plugin(({ addVariant }) =>
+ addVariant("phx-change-loading", [
+ ".phx-change-loading&",
+ ".phx-change-loading &",
+ ])
+ ),
+ ],
+};
diff --git a/phoenix-liveview/config/config.exs b/phoenix-liveview/config/config.exs
new file mode 100644
index 0000000..b1091ba
--- /dev/null
+++ b/phoenix-liveview/config/config.exs
@@ -0,0 +1,41 @@
+import Config
+
+config :webmcp_demo,
+ generators: [timestamp_type: :utc_datetime]
+
+config :webmcp_demo, WebmcpDemoWeb.Endpoint,
+ url: [host: "localhost"],
+ adapter: Bandit.PhoenixAdapter,
+ render_errors: [
+ formats: [html: WebmcpDemoWeb.ErrorHTML, json: WebmcpDemoWeb.ErrorJSON],
+ layout: false
+ ],
+ pubsub_server: WebmcpDemo.PubSub,
+ live_view: [signing_salt: "webmcp_demo_salt"]
+
+config :esbuild,
+ version: "0.17.11",
+ webmcp_demo: [
+ args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+ cd: Path.expand("../assets", __DIR__),
+ env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
+ ]
+
+config :tailwind,
+ version: "3.4.3",
+ webmcp_demo: [
+ args: ~w(
+ --config=tailwind.config.js
+ --input=css/app.css
+ --output=../priv/static/assets/app.css
+ ),
+ cd: Path.expand("../assets", __DIR__)
+ ]
+
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+config :phoenix, :json_library, Jason
+
+import_config "#{config_env()}.exs"
diff --git a/phoenix-liveview/config/dev.exs b/phoenix-liveview/config/dev.exs
new file mode 100644
index 0000000..21ec195
--- /dev/null
+++ b/phoenix-liveview/config/dev.exs
@@ -0,0 +1,28 @@
+import Config
+
+config :webmcp_demo, WebmcpDemoWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4000],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "webmcp_demo_dev_secret_key_base_that_is_at_least_64_bytes_long_for_security",
+ watchers: [
+ esbuild: {Esbuild, :install_and_run, [:webmcp_demo, ~w(--sourcemap=inline --watch)]},
+ tailwind: {Tailwind, :install_and_run, [:webmcp_demo, ~w(--watch)]}
+ ]
+
+config :webmcp_demo, WebmcpDemoWeb.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"lib/webmcp_demo_web/(controllers|live|components)/.*(ex|heex)$"
+ ]
+ ]
+
+config :webmcp_demo, dev_routes: true
+
+config :logger, :console, format: "[$level] $message\n"
+
+config :phoenix, :stacktrace_depth, 20
+
+config :phoenix, :plug_init_mode, :runtime
diff --git a/phoenix-liveview/config/prod.exs b/phoenix-liveview/config/prod.exs
new file mode 100644
index 0000000..9871c41
--- /dev/null
+++ b/phoenix-liveview/config/prod.exs
@@ -0,0 +1,6 @@
+import Config
+
+config :webmcp_demo, WebmcpDemoWeb.Endpoint,
+ cache_static_manifest: "priv/static/cache_manifest.json"
+
+config :logger, level: :info
diff --git a/phoenix-liveview/config/runtime.exs b/phoenix-liveview/config/runtime.exs
new file mode 100644
index 0000000..e672dd4
--- /dev/null
+++ b/phoenix-liveview/config/runtime.exs
@@ -0,0 +1,21 @@
+import Config
+
+if config_env() == :prod do
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :webmcp_demo, WebmcpDemoWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+end
diff --git a/phoenix-liveview/config/test.exs b/phoenix-liveview/config/test.exs
new file mode 100644
index 0000000..90d723f
--- /dev/null
+++ b/phoenix-liveview/config/test.exs
@@ -0,0 +1,10 @@
+import Config
+
+config :webmcp_demo, WebmcpDemoWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "test_secret_key_base_that_is_at_least_64_bytes_long_for_security_purposes",
+ server: false
+
+config :logger, level: :warning
+
+config :phoenix, :plug_init_mode, :runtime
diff --git a/phoenix-liveview/lib/webmcp_demo/application.ex b/phoenix-liveview/lib/webmcp_demo/application.ex
new file mode 100644
index 0000000..3b871c8
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo/application.ex
@@ -0,0 +1,23 @@
+defmodule WebmcpDemo.Application do
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [
+ WebmcpDemoWeb.Telemetry,
+ {Phoenix.PubSub, name: WebmcpDemo.PubSub},
+ WebmcpDemoWeb.Endpoint
+ ]
+
+ opts = [strategy: :one_for_one, name: WebmcpDemo.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ @impl true
+ def config_change(changed, _new, removed) do
+ WebmcpDemoWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/phoenix-liveview/lib/webmcp_demo_web.ex b/phoenix-liveview/lib/webmcp_demo_web.ex
new file mode 100644
index 0000000..fac07fb
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web.ex
@@ -0,0 +1,90 @@
+defmodule WebmcpDemoWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface.
+ """
+
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
+ quote do
+ use Phoenix.Router, helpers: false
+
+ import Plug.Conn
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
+ def controller do
+ quote do
+ use Phoenix.Controller,
+ formats: [:html, :json],
+ layouts: [html: WebmcpDemoWeb.Layouts]
+
+ import Plug.Conn
+
+ unquote(verified_routes())
+ end
+ end
+
+ def live_view do
+ quote do
+ use Phoenix.LiveView,
+ layout: {WebmcpDemoWeb.Layouts, :app}
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def html do
+ quote do
+ use Phoenix.Component
+
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ unquote(html_helpers())
+ end
+ end
+
+ defp html_helpers do
+ quote do
+ import Phoenix.HTML
+ import WebmcpDemoWeb.CoreComponents
+
+ alias Phoenix.LiveView.JS
+
+ unquote(verified_routes())
+ end
+ end
+
+ def verified_routes do
+ quote do
+ use Phoenix.VerifiedRoutes,
+ endpoint: WebmcpDemoWeb.Endpoint,
+ router: WebmcpDemoWeb.Router,
+ statics: WebmcpDemoWeb.static_paths()
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/live_view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/phoenix-liveview/lib/webmcp_demo_web/components/core_components.ex b/phoenix-liveview/lib/webmcp_demo_web/components/core_components.ex
new file mode 100644
index 0000000..bebd4c1
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/components/core_components.ex
@@ -0,0 +1,50 @@
+defmodule WebmcpDemoWeb.CoreComponents do
+ @moduledoc """
+ Core UI components for the WebMCP demo.
+ """
+ use Phoenix.Component
+
+ @doc """
+ Renders a notification message.
+ """
+ attr :type, :atom, default: :info, values: [:info, :success, :warning, :error]
+ attr :message, :string, required: true
+
+ def notification(assigns) do
+ ~H"""
+
+ <%= @message %>
+
+ """
+ end
+
+ @doc """
+ Renders a button.
+ """
+ attr :type, :string, default: "button"
+ attr :class, :string, default: ""
+ attr :rest, :global
+ slot :inner_block, required: true
+
+ def button(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc """
+ Renders a card component.
+ """
+ attr :class, :string, default: ""
+ slot :inner_block, required: true
+
+ def card(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+end
diff --git a/phoenix-liveview/lib/webmcp_demo_web/components/layouts.ex b/phoenix-liveview/lib/webmcp_demo_web/components/layouts.ex
new file mode 100644
index 0000000..0bfb84c
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/components/layouts.ex
@@ -0,0 +1,8 @@
+defmodule WebmcpDemoWeb.Layouts do
+ @moduledoc """
+ Layout components for the WebMCP demo application.
+ """
+ use WebmcpDemoWeb, :html
+
+ embed_templates "layouts/*"
+end
diff --git a/phoenix-liveview/lib/webmcp_demo_web/components/layouts/app.html.heex b/phoenix-liveview/lib/webmcp_demo_web/components/layouts/app.html.heex
new file mode 100644
index 0000000..c753bc6
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/components/layouts/app.html.heex
@@ -0,0 +1,3 @@
+
+ <%= @inner_content %>
+
diff --git a/phoenix-liveview/lib/webmcp_demo_web/components/layouts/root.html.heex b/phoenix-liveview/lib/webmcp_demo_web/components/layouts/root.html.heex
new file mode 100644
index 0000000..7f527f4
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/components/layouts/root.html.heex
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ <.live_title>WebMCP LiveView Demo
+
+
+
+
+ <%= @inner_content %>
+
+
diff --git a/phoenix-liveview/lib/webmcp_demo_web/controllers/error_html.ex b/phoenix-liveview/lib/webmcp_demo_web/controllers/error_html.ex
new file mode 100644
index 0000000..4d0123d
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/controllers/error_html.ex
@@ -0,0 +1,10 @@
+defmodule WebmcpDemoWeb.ErrorHTML do
+ @moduledoc """
+ Error HTML templates.
+ """
+ use WebmcpDemoWeb, :html
+
+ def render(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/phoenix-liveview/lib/webmcp_demo_web/controllers/error_json.ex b/phoenix-liveview/lib/webmcp_demo_web/controllers/error_json.ex
new file mode 100644
index 0000000..69e0640
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/controllers/error_json.ex
@@ -0,0 +1,9 @@
+defmodule WebmcpDemoWeb.ErrorJSON do
+ @moduledoc """
+ Error JSON responses.
+ """
+
+ def render(template, _assigns) do
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
+ end
+end
diff --git a/phoenix-liveview/lib/webmcp_demo_web/endpoint.ex b/phoenix-liveview/lib/webmcp_demo_web/endpoint.ex
new file mode 100644
index 0000000..0a2b312
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/endpoint.ex
@@ -0,0 +1,39 @@
+defmodule WebmcpDemoWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :webmcp_demo
+
+ @session_options [
+ store: :cookie,
+ key: "_webmcp_demo_key",
+ signing_salt: "webmcp_demo",
+ same_site: "Lax"
+ ]
+
+ socket "/live", Phoenix.LiveView.Socket,
+ websocket: [connect_info: [session: @session_options]],
+ longpoll: [connect_info: [session: @session_options]]
+
+ plug Plug.Static,
+ at: "/",
+ from: :webmcp_demo,
+ gzip: false,
+ only: WebmcpDemoWeb.static_paths()
+
+ if code_reloading? do
+ socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
+ plug Phoenix.LiveReloader
+ plug Phoenix.CodeReloader
+ end
+
+ plug Plug.RequestId
+ plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
+
+ plug Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Phoenix.json_library()
+
+ plug Plug.MethodOverride
+ plug Plug.Head
+ plug Plug.Session, @session_options
+ plug WebmcpDemoWeb.Router
+end
diff --git a/phoenix-liveview/lib/webmcp_demo_web/live/counter_live.ex b/phoenix-liveview/lib/webmcp_demo_web/live/counter_live.ex
new file mode 100644
index 0000000..804d2ce
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/live/counter_live.ex
@@ -0,0 +1,223 @@
+defmodule WebmcpDemoWeb.CounterLive do
+ @moduledoc """
+ LiveView demonstrating WebMCP integration with Phoenix.
+
+ This LiveView exposes server-side state to AI agents through WebMCP tools.
+ The JavaScript hook registers tools that communicate with LiveView via events.
+ """
+ use WebmcpDemoWeb, :live_view
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok,
+ socket
+ |> assign(:count, 0)
+ |> assign(:items, [])
+ |> assign(:notifications, [])
+ |> assign(:last_action, nil)}
+ end
+
+ @impl true
+ def handle_event("increment", _params, socket) do
+ new_count = socket.assigns.count + 1
+ {:noreply, socket |> assign(:count, new_count) |> assign(:last_action, "incremented")}
+ end
+
+ @impl true
+ def handle_event("decrement", _params, socket) do
+ new_count = max(0, socket.assigns.count - 1)
+ {:noreply, socket |> assign(:count, new_count) |> assign(:last_action, "decremented")}
+ end
+
+ @impl true
+ def handle_event("set_count", %{"value" => value}, socket) do
+ case Integer.parse(to_string(value)) do
+ {num, _} when num >= 0 ->
+ {:noreply, socket |> assign(:count, num) |> assign(:last_action, "set to #{num}")}
+
+ _ ->
+ {:noreply, push_notification(socket, :error, "Invalid count value")}
+ end
+ end
+
+ @impl true
+ def handle_event("add_item", %{"name" => name}, socket) when byte_size(name) > 0 do
+ item = %{
+ id: System.unique_integer([:positive]),
+ name: name,
+ created_at: DateTime.utc_now()
+ }
+
+ items = socket.assigns.items ++ [item]
+
+ {:noreply,
+ socket
+ |> assign(:items, items)
+ |> assign(:last_action, "added item: #{name}")
+ |> push_notification(:success, "Added: #{name}")}
+ end
+
+ def handle_event("add_item", _params, socket) do
+ {:noreply, push_notification(socket, :error, "Item name cannot be empty")}
+ end
+
+ @impl true
+ def handle_event("remove_item", %{"id" => id}, socket) do
+ id = if is_binary(id), do: String.to_integer(id), else: id
+ {removed, items} = Enum.split_with(socket.assigns.items, &(&1.id == id))
+
+ case removed do
+ [item] ->
+ {:noreply,
+ socket
+ |> assign(:items, items)
+ |> assign(:last_action, "removed item: #{item.name}")
+ |> push_notification(:success, "Removed: #{item.name}")}
+
+ [] ->
+ {:noreply, push_notification(socket, :error, "Item not found")}
+ end
+ end
+
+ @impl true
+ def handle_event("clear_items", _params, socket) do
+ count = length(socket.assigns.items)
+
+ {:noreply,
+ socket
+ |> assign(:items, [])
+ |> assign(:last_action, "cleared #{count} items")
+ |> push_notification(:success, "Cleared #{count} items")}
+ end
+
+ @impl true
+ def handle_event("dismiss_notification", %{"index" => index}, socket) do
+ index = if is_binary(index), do: String.to_integer(index), else: index
+ notifications = List.delete_at(socket.assigns.notifications, index)
+ {:noreply, assign(socket, :notifications, notifications)}
+ end
+
+ @impl true
+ def handle_event("get_state", _params, socket) do
+ state = %{
+ count: socket.assigns.count,
+ items: Enum.map(socket.assigns.items, &Map.take(&1, [:id, :name])),
+ item_count: length(socket.assigns.items),
+ last_action: socket.assigns.last_action
+ }
+
+ {:reply, state, socket}
+ end
+
+ defp push_notification(socket, type, message) do
+ notification = %{type: type, message: message, id: System.unique_integer([:positive])}
+ notifications = socket.assigns.notifications ++ [notification]
+ assign(socket, :notifications, notifications)
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+
+ <%= for {notification, index} <- Enum.with_index(@notifications) do %>
+
+ <%= notification.message %>
+
+
+ <% end %>
+
+
+
+
+
+
How This Works
+
+ This app exposes server-side state to AI agents via WebMCP:
+
+
+ - Connect any WebMCP-compatible client
+ - Discover 6 tools for counter and item management
+ - AI controls trigger LiveView events
+ - Server state syncs to UI in real-time
+
+
+
+
+
+
+
+
+
Counter
+
+ <%= @count %>
+
+
+
+
+
+ <%= if @last_action do %>
+
Last action: <%= @last_action %>
+ <% end %>
+
+
+
+
+
+
Items (<%= length(@items) %>)
+ <%= if Enum.empty?(@items) do %>
+
No items yet. Use add_item tool to add some.
+ <% else %>
+
+ <%= for item <- @items do %>
+ -
+ <%= item.name %>
+ #<%= item.id %>
+
+
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+
+
+ """
+ end
+end
diff --git a/phoenix-liveview/lib/webmcp_demo_web/router.ex b/phoenix-liveview/lib/webmcp_demo_web/router.ex
new file mode 100644
index 0000000..46de753
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/router.ex
@@ -0,0 +1,18 @@
+defmodule WebmcpDemoWeb.Router do
+ use WebmcpDemoWeb, :router
+
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_live_flash
+ plug :put_root_layout, html: {WebmcpDemoWeb.Layouts, :root}
+ plug :protect_from_forgery
+ plug :put_secure_browser_headers
+ end
+
+ scope "/", WebmcpDemoWeb do
+ pipe_through :browser
+
+ live "/", CounterLive, :index
+ end
+end
diff --git a/phoenix-liveview/lib/webmcp_demo_web/telemetry.ex b/phoenix-liveview/lib/webmcp_demo_web/telemetry.ex
new file mode 100644
index 0000000..6b33199
--- /dev/null
+++ b/phoenix-liveview/lib/webmcp_demo_web/telemetry.ex
@@ -0,0 +1,56 @@
+defmodule WebmcpDemoWeb.Telemetry do
+ use Supervisor
+ import Telemetry.Metrics
+
+ def start_link(arg) do
+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+ end
+
+ @impl true
+ def init(_arg) do
+ children = [
+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
+ ]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+
+ def metrics do
+ [
+ summary("phoenix.endpoint.start.system_time",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.endpoint.stop.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.stop.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.live_view.mount.stop.duration",
+ unit: {:native, :millisecond},
+ tags: [:view]
+ ),
+ summary("phoenix.live_view.handle_params.stop.duration",
+ unit: {:native, :millisecond},
+ tags: [:view]
+ ),
+ summary("phoenix.live_view.handle_event.stop.duration",
+ unit: {:native, :millisecond},
+ tags: [:view, :event]
+ )
+ ]
+ end
+
+ defp periodic_measurements do
+ [
+ {__MODULE__, :measure_memory, []}
+ ]
+ end
+
+ def measure_memory do
+ :telemetry.execute([:webmcp_demo, :memory], %{
+ total: :erlang.memory(:total)
+ })
+ end
+end
diff --git a/phoenix-liveview/mix.exs b/phoenix-liveview/mix.exs
new file mode 100644
index 0000000..0a5170c
--- /dev/null
+++ b/phoenix-liveview/mix.exs
@@ -0,0 +1,55 @@
+defmodule WebmcpDemo.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :webmcp_demo,
+ version: "1.0.0",
+ elixir: "~> 1.14",
+ elixirc_paths: elixirc_paths(Mix.env()),
+ start_permanent: Mix.env() == :prod,
+ aliases: aliases(),
+ deps: deps()
+ ]
+ end
+
+ def application do
+ [
+ mod: {WebmcpDemo.Application, []},
+ extra_applications: [:logger, :runtime_tools]
+ ]
+ end
+
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ defp deps do
+ [
+ {:phoenix, "~> 1.7.14"},
+ {:phoenix_html, "~> 4.1"},
+ {:phoenix_live_reload, "~> 1.2", only: :dev},
+ {:phoenix_live_view, "~> 1.0.0"},
+ {:floki, ">= 0.30.0", only: :test},
+ {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
+ {:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
+ {:telemetry_metrics, "~> 1.0"},
+ {:telemetry_poller, "~> 1.0"},
+ {:jason, "~> 1.2"},
+ {:plug_cowboy, "~> 2.7"},
+ {:bandit, "~> 1.5"}
+ ]
+ end
+
+ defp aliases do
+ [
+ setup: ["deps.get", "assets.setup", "assets.build"],
+ "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing", "cmd npm install --prefix assets"],
+ "assets.build": ["tailwind webmcp_demo", "esbuild webmcp_demo"],
+ "assets.deploy": [
+ "tailwind webmcp_demo --minify",
+ "esbuild webmcp_demo --minify",
+ "phx.digest"
+ ]
+ ]
+ end
+end
diff --git a/phoenix-liveview/package.json b/phoenix-liveview/package.json
new file mode 100644
index 0000000..43d6cd1
--- /dev/null
+++ b/phoenix-liveview/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "phoenix-liveview-webmcp-example",
+ "version": "1.0.0",
+ "description": "Phoenix LiveView example using the modern WebMCP API",
+ "type": "module",
+ "scripts": {
+ "dev": "cd assets && npm install && cd .. && mix setup && mix phx.server",
+ "build": "mix assets.deploy",
+ "typecheck": "echo 'Elixir uses dialyzer for type checking'",
+ "lint": "mix format --check-formatted"
+ },
+ "keywords": ["phoenix", "liveview", "webmcp", "mcp", "elixir"],
+ "license": "MIT"
+}