DEV Community

linou518
linou518

Posted on

Building a Mattermost Plugin to Switch AI Agent Models from the UI

Building a Mattermost Plugin to Switch AI Agent Models from the UI

TL;DR

Running 20+ AI agents means "switch this agent to GPT-4o" or "put everyone back on Claude" happens constantly. Editing config files and restarting gateways every time is painful. So I built a custom Mattermost plugin that lets you switch models with one click from the channel header.

Why a Mattermost Plugin?

Our multi-agent system uses Mattermost as the communication backbone. The human operator chats with each agent via DM, and agents coordinate with each other the same way. Mattermost is already the de facto management console.

The requirements for model switching were:

  1. Change an agent's model from the Mattermost UI — no SSH, no config files
  2. Changes take effect without restarting the agent's gateway
  3. Current model visible at a glance

"Build it as a Mattermost plugin" was the obvious answer.

Architecture

[Mattermost UI]
    ↓ Channel header button click
[Plugin Frontend (React)]
    ↓ API call
[Plugin Backend (Go)]
    ↓ Persist to KVStore
    ↓ (Original plan) Push notification to agent via message bus → Failed
    ↓ (Current) Agent pulls KV on each session start
[Agent (OpenClaw)]
    ↓ session_status(model=xxx) to switch
Enter fullscreen mode Exit fullscreen mode

KVStore: Built-in Key-Value Storage

Mattermost plugins come with KVStore — a persistent key-value store backed by the database. No separate SQLite or table migrations needed. Just plugin.API.KVSet(key, value) / KVGet(key).

Model preferences are stored here. Key: model_preference, value as JSON:

{
  "model": "anthropic/claude-opus-4-6",
  "updated_at": "2026-03-27T13:22:00+09:00",
  "updated_by": "linou"
}
Enter fullscreen mode Exit fullscreen mode

Frontend: Button in the Channel Header

Using Mattermost's channel_header_button extension point, an icon button sits in the channel header. Click it to open a modal with a dropdown of available models, with the current one highlighted.

registry.registerChannelHeaderButtonAction(
    <ModelSwitcherIcon />,
    (channel) => openModelSwitcherModal(channel),
    'Switch AI Model'
);
Enter fullscreen mode Exit fullscreen mode

Backend: Go REST API

func (p *Plugin) handleModelGet(w http.ResponseWriter, r *http.Request) {
    data, _ := p.API.KVGet("model_preference")
    w.Header().Set("Content-Type", "application/json")
    w.Write(data)
}

func (p *Plugin) handleModelSet(w http.ResponseWriter, r *http.Request) {
    var req ModelRequest
    json.NewDecoder(r.Body).Decode(&req)
    data, _ := json.Marshal(req)
    p.API.KVSet("model_preference", data)
    w.WriteHeader(http.StatusOK)
}
Enter fullscreen mode Exit fullscreen mode

Endpoints:

  • GET /plugins/.../api/v1/model — Get current model
  • PUT /plugins/.../api/v1/model — Set model

The Hardest Part: The Notification Wall

The original design called for real-time push notifications to agents when the model changes. After writing to KVStore, the plugin would POST to our internal message bus, which would forward to the target agent.

It didn't work. The problem: Docker networking. Mattermost runs inside a container. The message bus runs on a separate host. HTTP from inside the container to the host network was blocked by firewall rules.

[Mattermost Container] --X--> [Host Network: Message Bus:8091]
Enter fullscreen mode Exit fullscreen mode

Setting network_mode: host in Docker Compose would fix it, but I didn't want to break the official Compose configuration. A reverse proxy was another option, but overkill for this.

The Fix: Switch to Pull

I gave up on push (plugin → agent) and switched to pull (agent → plugin).

Each agent fetches the current model setting from the plugin API at the start of every session:

curl -s "http://mattermost-server:8065/plugins/.../api/v1/model" \
  -H "Authorization: Bearer <token>"
Enter fullscreen mode Exit fullscreen mode

If the returned model differs from the current session, session_status(model=xxx) handles the switch. This check is codified in each agent's AGENTS.md as a mandatory session startup step.

The result:

  • Zero dependency on push infrastructure
  • Model switches take effect on the next session start (not instant, but perfectly practical)
  • Implementation became drastically simpler

Bonus Feature: RHS File Browser

The same plugin also includes a file browser in Mattermost's Right-Hand Sidebar (RHS). It displays the shared directory structure and lets you browse files without SSHing into a machine.

This uses registerRightHandSidebarComponent to mount a React component in the RHS. The backend serves the directory tree of /mnt/shared/ as a JSON API.

The gotcha here was toggleRHSPlugin. It's a Redux action, not a regular function call:

// ❌ Doesn't work
toggleRHSPlugin();

// ✅ Correct
store.dispatch(toggleRHSPlugin());
Enter fullscreen mode Exit fullscreen mode

Mattermost's plugin frontend documentation is sparse on this. Had to read the source code.

After Three Days of Use

Being able to switch models from the UI turned out to be more convenient than expected. Especially when bouncing between Claude Opus for heavy reasoning tasks and Sonnet for daily conversations — one click does it.

KVStore + pull architecture loses on real-time responsiveness but wins on stability and simplicity. In a multi-agent system, "hard to break" trumps everything.

Key Takeaways

  • Mattermost plugins work surprisingly well as agent management UIs
  • KVStore is perfect for persisting settings (no external DB needed)
  • When push is hard, pull is often the pragmatic choice
  • toggleRHSPlugin is a Redux action — must use store.dispatch()

If you're using Mattermost as your agent infrastructure, custom plugins are worth exploring. The official docs are minimal, but starting from mattermost-plugin-starter-template on GitHub gets you something working in a day.

Top comments (0)