woow_emqx_mcp_server
The Woow EMQX MCP Server provides AI assistants with 39 curated tools to securely monitor, manage, and operate an EMQX MQTT broker. It includes a token-authenticated interface, a web admin GUI, and a reverse proxy in a single container.
Cluster & Monitoring: Check cluster health, node details, live stats, metrics (current/historical), alarms, and Prometheus data.
Client Management: List, inspect, and manage clients; kick (disconnect), subscribe, or unsubscribe clients.
Topics & Subscriptions: List routed topics and subscriptions; find subscribers for a specific topic.
Messaging: Publish single or bulk messages (up to 50); list, read, and delete retained messages.
Access Control: Manage authenticators, MQTT users, authorization sources, ACL rules, and bans (ban/unban).
Diagnostics: Create, read, and delete packet traces; list listeners and traces.
Data Integration: List, toggle, and test rule-engine rules; view rule metrics; inspect connectors and actions.
Safety & Control: 13 destructive tools are explicitly marked; tools can be restricted by category, operation, or read-only mode.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@woow_emqx_mcp_serverlist all connected clients and their subscriptions"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Overview
Woow EMQX MCP Server turns an EMQX MQTT broker into something an AI assistant can operate safely. It exposes the EMQX REST API v5 as 39 MCP tools, puts a token-authenticated reverse proxy in front of them, and ships a React admin console for configuring the lot — all in a single container.
Ask Claude "why did my sensor stop reporting?" and it can list the client, check whether the session is still alive, look at what the client is subscribed to, confirm the topic has a route, read the retained payload, and tell you the device never reconnected after its last will fired — without you opening a dashboard.
This bundle is the EMQX sibling of
woow_n8n_mcp_server and shares its
mcp_admin_core foundation, so the operational model, GUI and proxy semantics are identical
across the WOOWTECH MCP fleet.
Why This Bundle?
Challenge | Solution |
The EMQX REST API is large and easy to misuse from an LLM | 39 curated tools with descriptions written for models, not humans |
An open MCP endpoint would let anything drive your broker | Token-authenticated reverse proxy; the MCP server binds to |
"Give the AI everything" is not an acceptable security posture | Three-level gating — category, tool, and per-operation — plus a read-only master switch |
Destructive operations look identical to reads over MCP | Every tool carries MCP annotations; |
Tool configuration means editing env vars and restarting | Visual toggles in the GUI; changes are written into the subprocess env and applied on restart |
Debugging a broken MCP server means | Live SSE log streaming with search, in the browser |
Claude's connector refuses no-auth remote servers | Two ready-made Cloudflare Workers: an OAuth 2.1 gateway and a clean-404 public endpoint |
Before vs. After
Aspect | Raw EMQX REST API | With This Bundle |
AI access | Hand-rolled HTTP calls per assistant | One MCP URL, 39 typed tools |
Auth | API key/secret pasted into every client | Key stored once, proxy token handed out instead |
Blast radius | Full API surface | Whatever the switches allow |
Errors | Raw 4xx bodies | Messages that tell the model what to do next |
Observability | Broker logs only | Dashboard health + streaming MCP logs |
Deployment | N/A | Single container: Docker, Podman, or Kubernetes |
Related MCP server: Kafka MCP Server
Features
Dashboard
Aggregated health for the whole stack in one view — the MCP server subprocess (with PID), the EMQX broker connection (with the REST URL it is using), and the built-in reverse proxy. Below that, the MCP Admin version, the EMQX cluster node name, and the live count of connected MQTT clients.
Tool Manager
All 39 tools grouped into 7 categories, each with a toggle. The header shows the enabled count
(39 of 39 tools enabled), and a search box filters by name, description or category. Dangerous
tools are marked. Toggling writes the disabled set into the MCP server's environment and restarts
the subprocess, so the change reaches the live MCP surface — a disabled tool disappears from
tools/list entirely rather than failing at call time.
Connection Configuration
Point the bundle at your broker: the EMQX Dashboard URL (base only — /api/v5 is appended for
you), API Key and API Secret. The secret is write-only; leaving it blank keeps the stored value,
because EMQX shows a secret exactly once, at creation. Test Connection performs a real call
and reports the broker edition, version and node count.
Permission Editor
A JSON policy editor for allowed_tools / denied_tools, for when a per-tool toggle is too
coarse or you want the policy under version control. Denying a tool here removes it from the MCP
surface the same way the toggle does.
Token Manager
The MCP proxy token: masked display, one-click rotation, and the proxy URL template. Rotating generates a new token, applies it, and restarts the proxy in one step.
Log Viewer
Live SSE stream of the MCP server subprocess with an in-memory ring buffer, text/regex search, pause, auto-scroll and clear. Every line is structured — timestamp, level, message, source — so the viewer can colour and filter them.
Settings
Full process control for the MCP subprocess (command, args, port, environment, restart), the proxy (timeout up to 24 h, optional upstream bearer), and the admin password.
Edge Deployment (Cloudflare)
Two Workers in cloudflare/ solve the two ways a remote MCP endpoint typically fails
to connect from Claude:
mcp-oauth-gateway.js— a full OAuth 2.1 authorization server at the edge: RFC 9728 protected-resource metadata, RFC 8414 AS metadata, RFC 7591 dynamic client registration, PKCE S256 authorize/token, and refresh-token rotation. The upstream proxy token never leaves the edge.mcp-direct.js— a dedicated hostname that serves the proxy and answers every other path, including/.well-known/*, with a clean JSON 404. That 404 is what lets a connector conclude "no sign-in service here" and fall back to an anonymous connection.
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Woow EMQX MCP Admin Bundle │
│ (Single Container) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ React SPA (Vite + Tailwind CSS) │ │
│ │ │ │
│ │ Dashboard │ Tools │ Connection │ Tokens │ Logs │ Permissions │ │
│ └──────────────────────────┬────────────────────────────────────┘ │
│ │ HTTP │
│ ┌──────────────────────────▼────────────────────────────────────┐ │
│ │ FastAPI Backend (:8080) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │ │
│ │ │ Auth │ │ Config │ │ Process │ │ MCP Proxy │ │ │
│ │ │Middleware│ │ Store │ │ Manager │ │ /private_* │ │ │
│ │ └──────────┘ └──────────┘ └───────────┘ └──────┬───────┘ │ │
│ └───────────────────────────────────────────────────│──────────┘ │
│ │ │
│ ┌───────────────────────────────────────────────────▼──────────┐ │
│ │ emqx_mcp_server (FastMCP, 127.0.0.1:3000) │ │
│ │ │ │
│ │ ToolGate │ 39 tools / 7 categories │ Streamable HTTP │ │
│ └──────────────────────────┬────────────────────────────────────┘ │
│ │ httpx (pooled, Basic auth) │
├─────────────────────────────┼──────────────────────────────────────┤
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ EMQX REST API v5 (:18083/api/v5) │ │
│ │ Clients │ Topics │ Retained │ AuthN/AuthZ │ Rules │ Traces │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘Request path, end to end
sequenceDiagram
autonumber
participant AI as AI assistant
participant CF as Cloudflare Worker
participant PX as MCP proxy<br/>/private_{token}/mcp
participant MCP as emqx_mcp_server<br/>127.0.0.1:3000
participant EMQX as EMQX REST API v5
AI->>CF: POST /mcp (initialize)
Note over CF: OAuth bearer checked,<br/>private path injected
CF->>PX: POST /private_{token}/mcp/
PX->>MCP: forward, strip hop-by-hop headers
MCP-->>PX: capabilities + mcp-session-id
PX-->>CF: 200 text/event-stream
CF-->>AI: session established
AI->>CF: tools/call emqx_list_clients
CF->>PX: forward
PX->>MCP: forward
Note over MCP: ToolGate check:<br/>category / tool / operation
MCP->>EMQX: GET /api/v5/clients?limit=50
EMQX-->>MCP: 200 JSON
Note over MCP: shape into a typed result
MCP-->>AI: structured contentThree-level tool gating
Every call passes the same gate. A tool that fails any level is never registered, so it does not
appear in tools/list at all — the model cannot even attempt it.
flowchart TD
A[Tool registration] --> B{Explicitly disabled?}
B -- yes --> X[Not registered]
B -- no --> C{Read-only mode<br/>and tool is dangerous?}
C -- yes --> X
C -- no --> D{Category disabled?}
D -- yes --> X
D -- no --> E{Any operation<br/>still allowed?}
E -- no --> X
E -- yes --> F[Registered on the MCP surface]
F --> G[Call arrives]
G --> H{Operation allowed<br/>for this call?}
H -- no --> Y[ToolError: operation switched off]
H -- yes --> Z[EMQX REST call]
style X fill:#FDECEC,stroke:#B4231F,color:#B4231F
style Y fill:#FDECEC,stroke:#B4231F,color:#B4231F
style F fill:#E8F0FF,stroke:#6183FC,color:#25408F
style Z fill:#E6F7F0,stroke:#00B173,color:#046B48Module dependency graph
graph TD
subgraph repo["Woow_emqx_mcp_server"]
SRV["emqx_mcp_server<br/><i>FastMCP server, 39 tools</i>"]
ADM["emqx_mcp_admin<br/><i>EMQX-specific admin routers</i>"]
CORE["mcp_admin_core<br/><i>shared: app, proxy, process, auth, config</i>"]
FE["frontend<br/><i>React 19 SPA</i>"]
OVR["frontend-overrides<br/><i>EMQX ConnectionConfig page</i>"]
CFW["cloudflare<br/><i>OAuth gateway + public endpoint</i>"]
end
REG["registry.py<br/><i>single source of truth</i>"]
SRV --> REG
ADM --> REG
ADM --> CORE
FE --> CORE
OVR --> FE
CORE --> SRV
CFW -.proxies.-> CORE
style REG fill:#E8F0FF,stroke:#6183FC,color:#25408F
style CORE fill:#F5F6F8,stroke:#9AA0AAregistry.py is deliberately shared: the GUI's tool list and the MCP server's registration loop
read the same 39 ToolSpec entries, so the console can never show a tool the server does not
serve. A test (tests/test_mcp_surface.py) asserts that parity.
Deployment topology
graph LR
subgraph internet["Public internet"]
CLIENT["Claude / Cursor /<br/>any MCP client"]
end
subgraph cf["Cloudflare"]
W1["Worker: OAuth 2.1 gateway"]
W2["Worker: clean-404 endpoint"]
TUN["Tunnel"]
end
subgraph k8s["k3s cluster"]
POD["emqx-mcp-admin pod<br/>:8080 admin + proxy<br/>:3000 MCP (localhost)"]
end
subgraph lan["LAN"]
BROKER["EMQX 5.8.8<br/>:1883 :8883 :8083 :8084<br/>:18083 REST"]
end
CLIENT -->|HTTPS| W1
CLIENT -->|HTTPS| W2
W1 --> TUN
W2 --> TUN
TUN --> POD
POD -->|REST v5| BROKER
style W1 fill:#E8F0FF,stroke:#6183FC,color:#25408F
style W2 fill:#E8F0FF,stroke:#6183FC,color:#25408F
style BROKER fill:#E6F7F0,stroke:#00B173,color:#046B48More detail, including the failure modes each layer absorbs, is in docs/architecture.md.
The 39 Tools
Seven categories. 13 tools are destructive and carry destructiveHint in their MCP
annotations; the rest are marked readOnlyHint. Tools with an Operations column can be
restricted further — you can allow read on emqx_manage_authn_users while denying create and
delete.
Cluster & Monitoring (7)
Tool | Description |
| Cluster nodes with version, uptime, CPU and memory |
| Full detail for one node, including load and connection counts |
| Live counters: connections, sessions, subscriptions, topics |
| Current throughput gauges used by the dashboard charts |
| Time-series metrics over a recent window |
| Active and historical broker alarms |
| Raw Prometheus exposition text for scraping or diffing |
Client Management (6)
Tool | Description | Destructive | Operations |
| MQTT clients known to the broker, with filters | read | |
| Full session detail for a single client | read | |
| Topics a specific client is subscribed to | read | |
| Forcibly disconnect a client and clear its session | ⚠ | delete |
| Subscribe a client to a topic on its behalf | ⚠ | create |
| Remove a subscription from a client on its behalf | ⚠ | delete |
Topics & Subscriptions (2)
Tool | Description |
| Routed topics currently present in the cluster |
| All subscriptions cluster-wide, filterable by topic or client |
emqx_list_subscriptions accepts match_topic: give it a concrete topic and it tells you exactly
which subscribers would receive a message published there. That single parameter answers most
"I published and nothing happened" questions.
Messaging (5)
Tool | Description | Destructive | Operations |
| Publish one MQTT message through the broker | ⚠ | create |
| Publish a batch of messages in one call (max 50) | ⚠ | create |
| Retained messages the broker is holding | read | |
| The retained message stored for one topic | read | |
| Delete the retained message for one topic | ⚠ | delete |
Three real-broker behaviours are handled here rather than left to the model: EMQX answers 202
with reason_code 16 when a publish had no subscribers (surfaced as
delivered_to_subscribers: false), retained payloads come back base64-encoded (decoded
transparently), and EMQX rejects a slash inside the retained-message path segment, so hierarchical
topics fall back to a listing scan for reads and an empty retained publish for deletes.
Access Control (8)
Tool | Description | Destructive | Operations |
| Authenticator chain and each authenticator's status | read | |
| List, create and delete MQTT users in the built-in database | ⚠ | create, delete, read |
| Authorization sources and their order in the chain | read | |
| Global authorization behaviour: no-match, deny action, cache | read | |
| Read and write built-in-database ACL rules | ⚠ | create, delete, read |
| Current ban list entries | read | |
| Ban a client id, username or IP from connecting | ⚠ | create |
| Lift a ban | ⚠ | delete |
Diagnostics (5)
Tool | Description | Destructive | Operations |
| Packet traces currently defined on the broker | read | |
| Start a packet trace for a client, topic or IP | ⚠ | create |
| Read the captured log for a trace | read | |
| Delete a trace; unflushed events are lost | ⚠ | delete |
| Listeners with bind addresses and running state | read |
Data Integration (6)
Tool | Description | Destructive | Operations |
| Rule-engine rules with their SQL and enabled state | read | |
| Match, pass and failure counters for one rule | read | |
| Enable or disable a rule | ⚠ | update |
| Dry-run rule SQL against a sample event, saving nothing | read | |
| Data-integration connectors and their health | read | |
| Outbound actions (bridges) and their health | read |
Screenshots
All screenshots are from a live deployment: EMQX 5.8.8 Opensource, node
emqx@woowtechshowha.local, reached over the LAN at 192.168.2.189:18083, with the bundle running
in k3s behind a Cloudflare Tunnel.
Login
JWT authentication with the admin password. The token is stored in an httpOnly cookie; every
/api/* route except the login endpoint and /healthz requires it.
Dashboard
Three status cards across the top — MCP SERVER (Online, with the subprocess PID), EMQX
BROKER (Connected, showing the REST base URL actually in use) and MCP PROXY (Active). The
Details row underneath carries the MCP Admin version, the EMQX cluster node name
(emqx@woowtechshowha.local) and the count of connected MQTT clients, read live from
/api/v5/stats.
Tool Manager
39 of 39 tools enabled, grouped by category with a per-tool toggle and a search box. Access
Control is expanded here, showing all eight of its tools with the one-line descriptions the model
also sees. Switching a tool off rewrites the subprocess environment and restarts it, so the tool
leaves tools/list — it is not merely blocked at call time.
EMQX Connection
The broker's REST endpoint and credentials. Only the base URL is entered — /api/v5 is appended
automatically, which is the single most common configuration mistake this removes. The API Secret
field is masked and write-only: leave it blank and the stored secret is kept, because EMQX only
ever shows a secret once, at creation time.
Token Manager
The MCP proxy token, masked to first and last four characters, with the proxy URL template underneath and one-click rotation. Rotating generates, applies and restarts in a single action.
Permission Editor
The same gating expressed as a JSON policy — allowed_tools and denied_tools — for when you
want the policy reviewable in a diff rather than clicked into a form. Format, Reset and Save;
denying a tool here removes it from the MCP surface exactly as the toggle does.
Log Viewer
Live SSE stream from the MCP subprocess, connected and holding 200 lines. Each line carries its
own level parsed from the child process — here, a run of POST /mcp/ requests from a connected
assistant, with 202 Accepted for notifications and 200 OK for calls. Pause, auto-scroll, clear
and a filter box sit above a 5000-line ring buffer.
Settings
Process control for the MCP subprocess: running state, PID, restart count, and the exact command
line — python3 -m emqx_mcp_server.server --transport http --host 127.0.0.1 --port 3000. Binding
to 127.0.0.1 is deliberate: the MCP server is reachable only through the authenticated proxy,
never directly. Proxy timeout and admin password live further down the page.
Installation
Prerequisites
EMQX 5.8+ with the Dashboard REST API reachable (default port
18083)An EMQX API Key / Secret pair (EMQX Dashboard → System → API Key)
Docker, Podman or Kubernetes
For development: Python 3.12+ and Node.js 20
Option 1: Docker / Podman
git clone https://github.com/WOOWTECH/Woow_emqx_mcp_server.git
cd Woow_emqx_mcp_server
docker build -t emqx-mcp-admin .
docker run -d \
--name emqx-mcp-admin \
-p 8080:8080 \
-v ./data:/data \
emqx-mcp-adminOpen http://localhost:8080 and log in. Configure the broker on the Connection page.
Option 2: Docker Compose
docker compose up -dOption 3: Kubernetes
kubectl apply -f k8s-deploy.yaml
kubectl -n emqx-mcp get podsThe manifest creates the namespace, a Secret holding config.json, a Deployment with readiness
and liveness probes on /healthz, and a ClusterIP Service on :8080. An init container seeds
/data/config.json from the Secret on every start, so the Secret is the source of truth.
Option 4: Development
pip install -e .
pip install -e ".[dev]"
cd frontend && npm install && npm run build && cd ..
export EMQX_MCP_BASE_URL=http://your-broker:18083
export EMQX_MCP_API_KEY=...
export EMQX_MCP_API_SECRET=...
# MCP server alone, over stdio
python -m emqx_mcp_server.server
# or the full admin bundle
uvicorn emqx_mcp_admin.main:app --port 8080Configuration
All settings live in /data/config.json:
{
"admin_password": "change-me",
"mcp_auth_token": "32-char-url-safe-token",
"connection": {
"emqx_mcp_base_url": "http://192.168.2.189:18083",
"emqx_mcp_api_key": "your-api-key",
"emqx_mcp_api_secret": "your-api-secret"
},
"tools": {
"disabled_tools": [],
"disabled_categories": [],
"disabled_operations": {},
"readonly": false,
"permissions": { "allowed_tools": ["*"], "denied_tools": [] }
},
"mcp_server": {
"command": "python3",
"args": ["-m", "emqx_mcp_server.server", "--transport", "http",
"--host", "127.0.0.1", "--port", "3000"],
"port": 3000,
"env": {
"EMQX_MCP_DISABLED_CATEGORIES": "",
"EMQX_MCP_DISABLED_TOOLS": "",
"EMQX_MCP_DISABLED_OPERATIONS": "{}",
"EMQX_MCP_READONLY": "false"
}
},
"proxy": { "timeout": 86400, "bearer_token": "" },
"token_history": []
}Environment variables
Variable | Default | Description |
|
| Path to the configuration file |
| random | JWT signing secret — set this in production, or sessions break on restart |
|
| Admin session lifetime |
| — | EMQX dashboard base URL (no |
| — | EMQX API key |
| — | EMQX API secret |
|
| Master switch: drop every destructive tool |
| empty | Comma-separated tool names |
| empty | Comma-separated category names |
|
| JSON map of tool → disabled operations |
|
| Default page size for list tools |
|
| Hard cap on page size |
Connecting AI Assistants
The proxy exposes the MCP server at /private_{token}/mcp/. The trailing slash matters — the
upstream redirects the slashless form, and a cross-host redirect drops the request.
Direct, on your own network
http://your-server:8080/private_{token}/mcp/Claude Code / Cursor / any URL-based client
claude mcp add --transport http woow-emqx \
https://your-host/private_{token}/mcp/Claude's custom-connector UI
The connector runs OAuth discovery before it connects, so a bare proxy URL fails with "Couldn't
register with … sign-in service". Deploy one of the Workers in cloudflare/:
Worker | URL shape | Auth | Use when |
|
| path token only | You want the same shape as the rest of the WOOWTECH MCP fleet |
|
| OAuth 2.1 + password | The endpoint is public and you want real authentication |
Both keep the upstream token at the edge and normalise the trailing slash. See docs/architecture.md for why the clean 404 matters.
API Reference
Authentication
Method | Endpoint | Description |
|
| Authenticate with the admin password, returns a JWT |
Dashboard
Method | Endpoint | Description |
|
| Aggregated health: MCP server, broker, proxy, version, node, connections |
Connection
Method | Endpoint | Description |
|
| Current connection settings, secret masked |
|
| Update base URL, API key, API secret |
|
| Live connectivity test; returns edition, version, node count |
|
| Replace the allow/deny tool policy |
Tools
Method | Endpoint | Description |
|
| All 39 tools with category, description and enabled state |
|
| Apply a tool enable/disable set and restart the subprocess |
|
| Update per-tool disabled operations |
Tokens
Method | Endpoint | Description |
|
| Current token, masked, plus rotation history |
|
| Generate, apply and restart in one step |
|
| Set a specific token value |
Logs
Method | Endpoint | Description |
|
| SSE stream, replays the recent buffer then follows |
|
| Text or regex search across the ring buffer |
Settings
Method | Endpoint | Description |
|
| Full config with secrets masked |
|
| Replace one config section |
|
| Subprocess status: running, pid, restart count |
|
| Restart the MCP subprocess |
System
Method | Endpoint | Description |
|
| Kubernetes-compatible health check, unauthenticated |
|
| The MCP endpoint itself |
Security
Authentication layers
flowchart TD
subgraph L1["Layer 1 - Admin GUI"]
A1["POST /api/auth/login"] --> A2["JWT HS256, httpOnly cookie"]
A2 --> A3["every /api/* route<br/>except login and /healthz"]
end
subgraph L2["Layer 2 - MCP proxy"]
B1["/private_{token}/mcp/"] --> B2{"token matches<br/>config store?"}
B2 -- no --> B3["403 Forbidden"]
B2 -- yes --> B4["forward to 127.0.0.1:3000"]
end
subgraph L3["Layer 3 - Tool gate"]
C1["category / tool / operation"] --> C2["read-only master switch"]
end
subgraph L4["Layer 4 - EMQX"]
D1["API key + secret, HTTP Basic,<br/>never sent to the client"]
end
L1 --> L2 --> L3 --> L4
style B3 fill:#FDECEC,stroke:#B4231F,color:#B4231FSecurity properties
The MCP server is never directly exposed. It binds to
127.0.0.1:3000; the only route in is the token-authenticated proxy.EMQX credentials never reach the client. They live in the config store and are used server-side for HTTP Basic auth. The API secret is write-only through the API.
Constant-time password comparison via
secrets.compare_digest.Injected dependencies are stripped from the tool schema, so a model can neither see nor override the HTTP client.
Error messages are actionable, not leaky.
mask_error_detailsis on; unexpected exceptions become a generic message while deliberateToolErrors reach the model verbatim.Every response is
Cache-Control: no-store, so a CDN in front cannot serve one operator's view to another.Destructive tools are declared as such in MCP annotations, so a well-behaved client can ask for confirmation before calling them.
Hardening checklist
Change
admin_passwordon first login.Set
JWT_SECRETexplicitly.Rotate
mcp_auth_tokenafter handing it to anyone, and use the OAuth gateway for public exposure rather than the path token.Give the bundle an EMQX API key scoped to what it actually needs.
Turn on
readonlyfor assistants that only need to observe.Put the MCP endpoint on its own hostname — never share one with the admin GUI.
Testing
pip install -e ".[dev]"
pytest -vTest file | Covers | Tests |
| Three-level gating and the read-only switch | 7 |
| Registration, annotations, registry ↔ server parity | 6 |
|
| 4 |
| Config keys reach the subprocess as | 3 |
| Slash-in-topic fallbacks, base64 payloads | 3 |
| Log capture, classification and SSE framing | 1 |
| The pooled client survives more than one tool call | 2 |
Total | 26 passing, 11 live-broker tests skipped without credentials |
The live-broker tests run against a real EMQX when EMQX_MCP_BASE_URL, EMQX_MCP_API_KEY and
EMQX_MCP_API_SECRET are set.
Verified against a real deployment
Suite | Result |
GUI + MCP audit over public HTTPS (endpoints, shapes, EMQX semantics, behaviour, security) | 64 / 64 |
Cloudflare OAuth gateway (discovery, DCR, PKCE, tokens, security, MCP) | 29 / 29 |
Public endpoint shape (clean 404s, slash forms, token rejection, tool calls) | 15 / 15 |
Tech Stack
Component | Technology | Version |
MCP server | FastMCP | 3.4+ |
Backend | FastAPI + Uvicorn | 0.115+, Python 3.12 |
Frontend | React + Tailwind CSS + Vite | React 19, Tailwind 3.4, Vite 6 |
HTTP client | httpx | pooled |
Auth | PyJWT | HS256 |
Broker | EMQX | 5.8+ (tested on 5.8.8 Opensource) |
Transport | MCP Streamable HTTP | 2025-06-18 |
Edge | Cloudflare Workers | ES modules |
Container | Docker / Podman | multi-stage |
Orchestration | Kubernetes / k3s | v1.31+ |
Project Structure
Woow_emqx_mcp_server/
├── emqx_mcp_server/ # The MCP server
│ ├── server.py # build_server(): FastMCP app factory
│ ├── registry.py # 39 ToolSpecs - shared with the GUI
│ ├── gating.py # ToolGate: category / tool / operation
│ ├── deps.py # EmqxHttp: borrowed, non-closeable client handle
│ ├── errors.py # EMQX failures -> actionable ToolErrors
│ ├── lifespan.py # pooled httpx client
│ ├── models.py # typed results
│ ├── settings.py # EMQX_MCP_* settings
│ └── tools/ # cluster, clients, topics, messaging,
│ # security, diagnostics, integration
├── emqx_mcp_admin/ # EMQX-specific admin layer
│ ├── main.py # create_app(extra_routers=[...])
│ └── routers/ # config, tools, health, logs, tokens
├── mcp_admin_core/ # Shared core (app, proxy, process, auth, config)
├── frontend/ # React 19 SPA
├── frontend-overrides/ # EMQX ConnectionConfig page
├── cloudflare/ # Edge Workers
│ ├── mcp-oauth-gateway.js # OAuth 2.1 authorization server
│ └── mcp-direct.js # clean-404 public endpoint
├── docs/
│ ├── architecture.md
│ └── screenshots/
├── tests/ # 26 passing, 11 live-broker
├── Dockerfile
├── docker-compose.yml
├── k8s-deploy.yaml
├── pyproject.toml
└── README_zh-TW.mdChangelog
v1.0.0 (2026-08)
Initial release — full EMQX MCP Admin Bundle
39 MCP tools across 7 categories, built test-first with FastMCP 3.4
Three-level gating — category, tool and per-operation — plus a read-only master switch
Admin console — Dashboard, Tool Manager, Connection, Tokens, Permissions, Logs, Settings
MCP proxy — token-authenticated reverse proxy, MCP server bound to
127.0.0.1Cloudflare Workers — OAuth 2.1 gateway and clean-404 public endpoint
Live-broker fixes — retained topics containing slashes, base64 payloads, and the
202/reason_code 16"no matching subscribers" caseFix — FastMCP's
Dependsentered and exited the pooledhttpx.AsyncClient, closing it after the first tool; every second call in a session failed with "Cannot reopen a client instance". The provider now returnsEmqxHttp, a borrowed handle that is deliberately not a context manager. Pinned bytests/test_client_lifetime.pyFix — tool switches were written to the config file but never reached the subprocess environment, so toggles had no effect on the live MCP surface
Fix — log capture set the handler but not the logger level, so INFO lines were dropped by the root logger before reaching the buffer and the Log Viewer showed nothing
Testing — 26 unit tests, plus 64/64, 29/29 and 15/15 audits against a live EMQX 5.8.8
Support
Issues: GitHub Issues
Email: service@woowtech.io
Sibling project: woow_n8n_mcp_server
License
MIT — see LICENSE.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceAn MCP server that enables interaction with multiple Kubernetes clusters simultaneously, providing comprehensive tools for cluster management, resource operations, and diagnostics across different environments.
- AlicenseNot gradedqualityDmaintenanceAn MCP server that enables interaction with Kafka clusters to manage topics, monitor consumer groups, and stream messages. It provides a comprehensive suite of tools for broker metadata inspection and local Kafka user management.MIT
- AlicenseNot gradedqualityCmaintenanceA comprehensive MCP server that provides advanced Docker operations through a unified interface with 16 MCP tools and 25+ CLI aliases, enabling secure container lifecycle management, multi-container orchestration, registry publishing, and system maintenance.4515ISC
- AlicenseNot gradedqualityAmaintenanceAn MCP server that enables AI assistants to safely interact with Apache Kafka clusters, providing tools for topic management, message operations, consumer groups, and cluster information.3MIT
Related MCP Connectors
MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.
34 production API tools over one hosted MCP endpoint.
MCP server for Appcircle mobile CI/CD platform.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/WOOWTECH/Woow_emqx_mcp_server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server