josedario87 424afa060c feat: Improve WebMCP connection handling and tools management
WebMCP service:
- Add headless mode configuration
- Implement proper event handlers with unsubscribe support
- Add connection info tracking (channel, server, status, tools)
- Add destroyWebMCP for cleanup
- Improve connectWithToken to pass token directly

Canvas store:
- Add connection state (reconnecting, status, error, info)
- Add computed statusColor for UI feedback

Components:
- Add ConnectionDropdown for connection status display
- Add ToolsDropdown for tools management UI

Tool registry:
- Improve tool activation/deactivation logic
- Better error handling and logging
2026-02-13 18:06:45 -06:00
2026-02-13 03:10:06 -06:00
2026-02-13 03:10:06 -06:00
2026-02-13 03:10:06 -06:00
2026-02-13 03:10:06 -06:00

Agent UI

Dynamic canvas interface for Claude Code interaction via MCP (Model Context Protocol).

Overview

Agent UI provides a visual canvas where Claude Code can render dynamic Vue 3 components, HTML content, and interactive UIs in real-time. It bridges the gap between CLI-based AI assistance and rich visual interfaces.

Architecture

agent-ui/
├── frontend/          # Vue 3 + Vite + Pinia
│   └── src/
│       ├── components/    # Canvas, Toolbar, StatusBar
│       ├── services/      # Dynamic components system
│       ├── stores/        # Pinia state management
│       └── styles/        # Global CSS
├── server/            # Bun HTTP API + SQLite
└── .mcp.json          # MCP server configuration

Quick Start

# Install dependencies
npm install

# Start development (frontend + server)
npm start

Connecting to Claude Code

  1. Start the development server: npm start
  2. In Claude Code, run /mcp to reconnect
  3. Click the widget (bottom-right) in the browser
  4. Paste the token from Claude Code

Dynamic Canvas

The dynamic canvas system allows Claude Code to render fully-featured Vue 3 components at runtime.

MCP Tools

render_vue_component

Renders a Vue 3 component directly in the canvas.

{
  id: "my-counter",           // Unique component ID
  name: "MyCounter",          // Component name
  template: `<div>...</div>`, // Vue template
  setup: `...`,               // Composition API setup code
  style: `.class { ... }`,    // Scoped CSS
  props: ["title"],           // Props list
  imports: ["ref", "computed"], // Vue imports needed
  componentProps: { title: "Hello" }, // Props values
  mode: "replace" | "append"  // Render mode
}

save_vue_component

Saves a component to SQLite for later reuse.

{
  id: "my-component",     // Optional, auto-generated if empty
  name: "MyComponent",
  template: `...`,
  setup: `...`,
  style: `...`,
  props: [...],
  imports: [...]
}

load_vue_component

Loads and renders a previously saved component.

{
  id: "my-component",
  componentProps: { ... },
  mode: "replace" | "append"
}

list_vue_components

Lists all saved components in the database.

delete_vue_component

Deletes a component from the database.

{
  id: "my-component"
}

render_html

Renders raw HTML with script/style support (legacy, simpler option).

{
  html: "<div>...</div>",
  mode: "replace" | "append" | "prepend"
}

Setup Function API

Inside the setup string, components have access to:

Vue Reactivity (via imports)

ref, reactive, computed, watch, watchEffect,
onMounted, onUnmounted, nextTick, provide, inject, h

Props & Context

props    // Component props
ctx      // { emit, attrs, slots, expose }

Event Bus

$emit(event, ...args)     // Emit global event
$on(event, callback)      // Listen (returns unsubscribe fn)
$once(event, callback)    // Listen once
$off(event, callback)     // Stop listening

Utilities

$fetch(url, options)      // Native fetch
$nextTick(callback)       // Vue nextTick
useCanvasStore()          // Pinia store (shared with main app)

Components API

$components.load(id)      // Load component from DB
$components.list()        // List all components
$components.save(comp)    // Save component to DB

Example: Interactive Counter

// MCP call: render_vue_component
{
  id: "counter",
  name: "Counter",
  template: `
    <div class="counter">
      <h2>{{ title }}</h2>
      <p>Count: {{ count }}</p>
      <button @click="decrement">-</button>
      <button @click="increment">+</button>
      <p>Double: {{ doubled }}</p>
    </div>
  `,
  setup: `
    const count = ref(0);
    const doubled = computed(() => count.value * 2);

    const increment = () => count.value++;
    const decrement = () => count.value--;

    onMounted(() => {
      console.log('Counter mounted!');
    });

    return { count, doubled, increment, decrement, title: props.title };
  `,
  style: `
    .counter { padding: 20px; background: #1e1e28; border-radius: 8px; }
    .counter button { margin: 0 5px; padding: 8px 16px; }
  `,
  props: ["title"],
  imports: ["ref", "computed", "onMounted"],
  componentProps: { title: "My Counter" }
}

Example: Async Data Fetching

{
  id: "data-loader",
  name: "DataLoader",
  template: `
    <div class="loader">
      <p v-if="loading">Loading...</p>
      <pre v-else>{{ data }}</pre>
    </div>
  `,
  setup: `
    const loading = ref(true);
    const data = ref(null);

    // Async setup - uses Suspense internally
    const res = await $fetch('http://localhost:4101/api/health');
    data.value = await res.json();
    loading.value = false;

    return { loading, data };
  `,
  imports: ["ref"]
}

Example: Event Communication

// Component A - Emitter
{
  id: "emitter",
  name: "Emitter",
  template: `<button @click="send">Send Event</button>`,
  setup: `
    const send = () => {
      $emit('my-event', { message: 'Hello!', timestamp: Date.now() });
    };
    return { send };
  `
}

// Component B - Listener
{
  id: "listener",
  name: "Listener",
  template: `<div>Last event: {{ lastEvent }}</div>`,
  setup: `
    const lastEvent = ref('none');

    $on('my-event', (data) => {
      lastEvent.value = JSON.stringify(data);
    });

    return { lastEvent };
  `,
  imports: ["ref"]
}

Features

CSS Scoping

Styles are automatically scoped to prevent collisions:

/* You write: */
.btn { background: red; }

/* Generated: */
#canvas-content [data-v-abc123] .btn { background: red; }

Async Setup

Components can use await directly in setup - automatically wrapped in Suspense:

setup: `
  const data = await $fetch('/api/data');
  return { data };
`

Shared State

Components share Pinia stores with the main app:

setup: `
  const store = useCanvasStore();
  console.log(store.isConnected);  // Same state as main app
`

No App Overhead

Components are rendered using render() + createVNode() instead of creating separate Vue apps, sharing the main app's context efficiently.


Technical Notes

Vite Configuration

Runtime template compilation requires this alias in vite.config.ts:

resolve: {
  alias: {
    'vue': 'vue/dist/vue.esm-bundler.js'
  }
}

How It Works

  1. MCP Tool Call → Claude Code sends component definition via WebSocket
  2. Build ComponentbuildComponent() creates Vue component with dynamic setup function using new Function()
  3. CSS Scoping → Styles are transformed: .btn#canvas-content [data-v-xxx] .btn
  4. RendercreateVNode() + render() mounts component without creating new Vue app
  5. Context Inheritance → Component inherits appContext from main app (Pinia, directives, etc.)

File Structure

frontend/src/services/dynamicComponents.ts  (~300 lines)
├── EventBus         # Inter-component communication
├── CSS Scoping      # Style isolation
├── Components API   # CRUD with server
├── Vue Helpers      # What setup() receives
├── buildComponent   # Create component from definition
└── renderInlineComponent  # Mount to DOM

API Endpoints

Endpoint Method Description
/api/health GET Health check
/api/history GET Get tool execution history
/api/history POST Log tool execution
/api/history DELETE Clear history
/api/components GET List saved components
/api/components POST Save component
/api/components/:id GET Get component by ID
/api/components/:id DELETE Delete component
/api/config GET/POST Key-value config storage


Theme System

Agent UI includes a powerful theming engine with visual editor, presets, and design tokens for consistent styling across dynamic components.

Overview

The theme system provides:

  • Visual Editor: Edit CSS variables with live preview
  • Presets: System themes (Dark/Light) + custom user themes
  • Persistence: Themes saved to SQLite database
  • Export/Import: Share themes as JSON files
  • Design Tokens: Structured guide for LLM agents to follow

Accessing the Theme Editor

Navigate to /themes in the UI or click the palette icon in the sidebar.

Theme Structure

Themes are organized by categories:

{
  "colors": {
    "bg-primary": "#0f0f14",
    "bg-secondary": "#16161d",
    "bg-hover": "#1e1e28",
    "bg-tertiary": "#252530",
    "border-color": "#2a2a3a"
  },
  "text": {
    "text-primary": "#e4e4e7",
    "text-secondary": "#a1a1aa",
    "text-muted": "#52525b"
  },
  "accent": {
    "accent": "#6366f1",
    "accent-hover": "#818cf8",
    "accent-muted": "rgba(99, 102, 241, 0.2)",
    "accent-text": "#ffffff"
  },
  "semantic": {
    "success": "#22c55e",
    "success-bg": "rgba(34, 197, 94, 0.15)",
    "warning": "#eab308",
    "warning-bg": "rgba(234, 179, 8, 0.15)",
    "error": "#ef4444",
    "error-bg": "rgba(239, 68, 68, 0.15)",
    "info": "#3b82f6",
    "info-bg": "rgba(59, 130, 246, 0.15)"
  },
  "spacing": {
    "radius-sm": "4px",
    "radius-md": "8px",
    "radius-lg": "12px",
    "radius-full": "9999px"
  },
  "typography": {
    "font-sans": "Inter, system-ui, sans-serif",
    "font-mono": "JetBrains Mono, monospace"
  },
  "effects": {
    "shadow-sm": "0 1px 2px rgba(0,0,0,0.3)",
    "shadow-md": "0 4px 12px rgba(0,0,0,0.4)",
    "transition-fast": "0.15s ease"
  }
}

MCP Tools

get_design_tokens

Returns design tokens for the active theme. Use this to create components with consistent styling.

{
  category: "all" | "colors" | "text" | "accent" | "semantic" | "spacing" | "typography" | "effects"
}

Response example:

Design Tokens del tema "Dark":

[COLORS]
  --bg-primary: #0f0f14
  --bg-secondary: #16161d
  ...

GUÍA DE USO:
- Usa var(--nombre-variable) en CSS
- Los componentes dinámicos tienen acceso a $theme.getVariable('nombre')
- Colores semánticos: success, warning, error, info (con -bg para fondos)
- Radius: radius-sm (4px), radius-md (8px), radius-lg (12px), radius-full (9999px)

Theme API in Dynamic Components

Components have access to $theme helper:

// Get CSS variable value
$theme.getVariable('accent')  // "#6366f1"

// Set variable temporarily (runtime only)
$theme.setVariable('accent', '#ff0000')

// Get all design tokens
$theme.getTokens()

// Get active theme object
$theme.getActiveTheme()

// Get current variables
$theme.getVariables()

Example: Theme-Aware Component

{
  id: "themed-card",
  name: "ThemedCard",
  template: `
    <div class="card">
      <h3>{{ title }}</h3>
      <p>Current accent: {{ accentColor }}</p>
      <button @click="randomizeAccent">Randomize Accent</button>
    </div>
  `,
  setup: `
    const accentColor = ref($theme.getVariable('accent'));

    const randomizeAccent = () => {
      const hue = Math.floor(Math.random() * 360);
      const newColor = \`hsl(\${hue}, 70%, 60%)\`;
      $theme.setVariable('accent', newColor);
      accentColor.value = newColor;
    };

    return { title: props.title, accentColor, randomizeAccent };
  `,
  style: `
    .card {
      padding: var(--radius-lg);
      background: var(--bg-secondary);
      border: 1px solid var(--border-color);
      border-radius: var(--radius-md);
    }
    .card h3 { color: var(--text-primary); }
    .card p { color: var(--text-secondary); }
    .card button {
      background: var(--accent);
      color: var(--accent-text);
      border: none;
      padding: 0.5rem 1rem;
      border-radius: var(--radius-sm);
      cursor: pointer;
    }
    .card button:hover { background: var(--accent-hover); }
  `,
  props: ["title"],
  imports: ["ref"],
  componentProps: { title: "Themed Card" }
}

API Endpoints (Themes)

Endpoint Method Description
/api/themes GET List all themes
/api/themes POST Create/update theme
/api/themes/active GET Get active (default) theme
/api/themes/:id GET Get theme by ID
/api/themes/:id DELETE Delete theme
/api/themes/:id/default POST Set as default theme
/api/themes/export/:id GET Export theme as JSON
/api/design-tokens GET Get design tokens guide

Theme Editor Features

Desktop View

  • Sidebar: List of system and custom themes
  • Editor: Category tabs (Colors, Text, Accent, etc.)
  • Variables Grid: Color pickers and input fields
  • Live Preview: Buttons, cards, badges, inputs

Mobile View

  • Compact Header: Theme dropdown + action buttons
  • Collapsible Variables: Toggle to show/hide editors
  • Responsive Preview: Adapts to screen size

Actions

  • Save: Saves current changes (creates copy if editing system theme)
  • Reset: Reverts unsaved changes
  • Export: Downloads theme as JSON file
  • Clone: Creates a copy of any theme
  • Set Default: Makes theme load on startup

File Structure

frontend/src/
├── stores/theme.ts           # Pinia store
├── services/themeService.ts  # API client + utilities
├── pages/ThemesPage.vue      # Main editor page
└── components/themes/
    ├── ColorPicker.vue       # HSL color picker
    ├── VariableEditor.vue    # Variable input (color/size/text)
    ├── ThemePreview.vue      # Live preview component
    └── ThemeListItem.vue     # Theme list item with actions

Tech Stack

  • Frontend: Vue 3, Vite, Pinia, TypeScript
  • Server: Bun, SQLite
  • MCP: @nucleoriofrio/webmcp
  • PWA: vite-plugin-pwa

License

MIT

Description
Agent UI - WebMCP Canvas
Readme 3.3 MiB
Languages
Vue 58.4%
TypeScript 36.9%
Kotlin 3.1%
Python 0.6%
Rust 0.3%
Other 0.6%