Test WebSocket, Socket.IO, MQTT and SSE Clients With AI Agents
To let Claude Code, Codex or Cursor test your WebSocket, Socket.IO, MQTT or SSE client, give the agent a server it can control over MCP. With the Hooklistener MCP server the loop is: the agent calls create_realtime_endpoint to get a hosted URL, you (or the agent) point the client at it, the agent calls wait_for_realtime_message until the client sends what it should, answers like your server with send_realtime_message or auto-responder rules, and reads the whole session back with get_realtime_messages.
This guide covers setup, one walkthrough per protocol with the client code, simulated server behaviour and failure paths, how to run the same check in CI without an agent, the limits, and how this compares with Postman, wscat, websocat, public echo servers, the HiveMQ public broker and Mockoon.
The Loop in Six Steps
- 01
Connect your agent to the MCP server
Add https://app.hooklistener.com/api/mcp to Claude Code, Codex or Cursor and sign in with OAuth.
- 02
Create a hosted endpoint for your protocol
The agent calls create_realtime_endpoint with protocol websocket, socketio, mqtt or sse. The reply holds the connection URL, for example wss://app.hooklistener.com/ws/orders-feed-k3x9.
- 03
Point your client at it
Start your app or client with that URL in place of your real server's URL. Every connection becomes a recorded session.
- 04
Wait for what the client should send
wait_for_realtime_message blocks for up to 60 seconds until the client sends a message containing the text you expect, or until a client connects.
- 05
Answer like your server
The agent pushes events with send_realtime_message (event names for Socket.IO and SSE, topics for MQTT), or sets up auto-responder rules with manage_realtime_rules.
- 06
Read the session back and report
get_realtime_messages returns every frame in both directions, in order, so the agent can check what the client sent and how it reacted.
The endpoint plays the server; your client code is what gets tested. Hooklistener does not run your client. The agent starts it with its own tools (a shell command, a dev server, a Playwright browser), which keeps the test honest: the client connects over the public internet exactly as it would to your production server.
The protocol is fixed when the endpoint is created and decides the URL and what “event name” means:
| Protocol | protocol value | Client connects to | event_type means |
|---|---|---|---|
| WebSocket | websocket | wss://app.hooklistener.com/ws/<slug> | None (raw text or binary frames) |
| Socket.IO | socketio | https://app.hooklistener.com + path "/sio/<slug>" | Event name |
| MQTT over WebSocket | mqtt | wss://app.hooklistener.com/mqtt/<slug> | Topic |
| Server-Sent Events | sse | https://app.hooklistener.com/sse/<slug> | event: field (plus id:) |
Setup: Connect the MCP Server
The server is hosted at https://app.hooklistener.com/api/mcp. There is nothing to install: add the URL and sign in through your browser (OAuth). In Claude Code:
claude mcp add --transport http hooklistener https://app.hooklistener.com/api/mcp
# then in Claude Code: /mcp -> hooklistener -> authenticateClient-specific steps: Claude Code, Codex, Cursor. The free plan is enough for everything below.
Why your agent only sees create_realtime_endpoint at first
The server has 67 tools in seven toolsets, but a new workspace lists only 26: the webhook tools plus the one tool that starts each other product. For real-time endpoints that is create_realtime_endpoint. The rest of the realtime toolset (list_realtime_endpoints, list_realtime_sessions, get_realtime_messages, send_realtime_message, wait_for_realtime_message, manage_realtime_rules) is listed once your workspace has its first real-time endpoint and the client reconnects. The first create_realtime_endpoint reply says so. In Claude Code, run /mcp, pick hooklistener and choose Reconnect. The hidden tools can be called before that; they just aren't advertised.
To list them from the first message, send the x-hooklistener-toolsets header with realtime (or all, or a list such as realtime,email). OAuth keeps working; the header only changes which tools are listed.
claude mcp add --transport http hooklistener https://app.hooklistener.com/api/mcp \
--header "x-hooklistener-toolsets: realtime"{
"mcpServers": {
"hooklistener": {
"type": "http",
"url": "https://app.hooklistener.com/api/mcp",
"headers": { "x-hooklistener-toolsets": "realtime" }
}
}
}Cursor's .cursor/mcp.json takes the same url and headers keys without type. In Codex, static headers go in http_headers:
[mcp_servers.hooklistener]
url = "https://app.hooklistener.com/api/mcp"
oauth_resource = "https://app.hooklistener.com/api/mcp"
http_headers = { "x-hooklistener-toolsets" = "realtime" }How Waiting Works
wait_for_realtime_message blocks: the call stays open until a match arrives or the timeout expires (default 30 seconds, maximum 60), then returns message_received, session_connected or timeout. Its filters:
contains: text that must appear in the message payload (case-sensitive; event names and topics are not searched).direction:incoming(default, what the client sent),outgoing,any, orsessionto wait for a client to connect.session_id: only one connection.
A wait with a timeout only sees messages that arrive after it starts. Clients connect, subscribe and answer within milliseconds, usually before the agent's next tool call, so the reliable pattern is: start the client (or send your message), then call the wait with timeout: 0, which returns the newest matching message already recorded (or timeout at once), and only if nothing is there yet, wait again with a real timeout. Because the call blocks, the agent should start the client in the background (a background shell command or an already running dev server) before waiting. direction: "session" only sees new connections; for a client that is already connected, list_realtime_sessions with status: "open" finds it.
Protocol packets are recorded as incoming messages too: the Socket.IO namespace connect, and MQTT CONNECT, SUBSCRIBE and UNSUBSCRIBE. A wait with no contains can match one of those, so give it a specific string.
Walkthrough 1: A WebSocket Client That Subscribes and Handles a Pushed Event
A dashboard opens a WebSocket, subscribes to an orders channel and renders each order.created event the server pushes. The URL comes from an environment variable, so the test can swap it:
// Browser, or Node 22+ (global WebSocket)
const ws = new WebSocket(process.env.ORDERS_WS_URL);
ws.addEventListener("open", () => {
ws.send(JSON.stringify({ op: "subscribe", channel: "orders" }));
});
ws.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "order.created") {
console.log("new order", msg.id, msg.total);
ws.send(JSON.stringify({ op: "ack", id: msg.id }));
}
});Ask the agent:
> Create a Hooklistener WebSocket endpoint called "orders feed". Start
orders-client.js in the background with ORDERS_WS_URL set to its
connection URL. Check it subscribes to the orders channel, then push an
order.created event and confirm the client acknowledges it with the
same id. Show me the whole session at the end.First, create_realtime_endpoint. The slug is the name plus a short random suffix:
create_realtime_endpoint({ "name": "orders feed", "protocol": "websocket" })
{
"id": "5b1e0c7a-…",
"name": "orders feed",
"slug": "orders-feed-k3x9",
"protocol": "websocket",
"status": "active",
"connection_url": "wss://app.hooklistener.com/ws/orders-feed-k3x9",
"auth_token_required": false,
"proxy_enabled": false,
"console_url": "https://app.hooklistener.com/realtime/5b1e0c7a-…",
"toolset_hint": {
"toolset": "realtime",
"action": "reconnect_mcp_client",
"message": "Reconnect the MCP client to list the rest of the realtime toolset: …"
}
}The agent starts the client, then checks for the subscribe message:
wait_for_realtime_message({
"endpoint_id": "5b1e0c7a-…",
"contains": "subscribe",
"timeout": 0
})
{
"status": "message_received",
"message": {
"position": 1,
"session_id": "c2a7…",
"direction": "incoming",
"opcode": "text",
"payload": "{\"op\":\"subscribe\",\"channel\":\"orders\"}",
"payload_size": 37,
"truncated": false,
"event_type": null,
"source": "client",
"occurred_at": "2026-09-26T10:14:02Z"
}
}Now the agent acts as the server. Payloads can use template variables, rendered when the frame is sent:
send_realtime_message({
"endpoint_id": "5b1e0c7a-…",
"session_id": "c2a7…",
"payload": "{\"type\":\"order.created\",\"id\":\"ord_{{random.hex:6}}\",\"total\":4200,\"at\":\"{{now}}\"}"
})
{ "sent": true, "session_id": "c2a7…" }
wait_for_realtime_message({
"endpoint_id": "5b1e0c7a-…",
"session_id": "c2a7…",
"contains": "\"op\":\"ack\"",
"timeout": 0
})
// "timeout" status? The ack isn't there yet: call again with "timeout": 15Leave out session_id and the message is broadcast to every open session of the endpoint (the reply is { "broadcast": true, "sessions": 2 }). Finally, get_realtime_messages with the session ID returns the conversation in order, up to 500 messages per call with a before cursor for older ones. Each outgoing message carries its source (mcp, rule, on_connect and so on), so the report can say which side said what. A good report reads like: “Connected, subscribed to orders 40 ms after open, received order.created ord_3fa91c, acked with the same id 12 ms later.”
Binary frames:pass binary: true with a base64 payload to send a binary WebSocket frame. Binary frames your client sends are stored base64-encoded (the message has encoding: "base64").
Walkthrough 2: A Socket.IO Client That Emits and Receives Named Events
Create the endpoint with "protocol": "socketio". The reply's connection_url is https://app.hooklistener.com and socketio_path is /sio/support-chat-x8p2. The endpoint speaks the Socket.IO v5 protocol over Engine.IO v4 (the protocol of socket.io-client 3.x and 4.x) and only the WebSocket transport, so the client must skip long-polling:
import { io } from "socket.io-client";
const socket = io(process.env.CHAT_URL ?? "https://app.hooklistener.com", {
path: process.env.CHAT_PATH ?? "/sio/support-chat-x8p2",
transports: ["websocket"],
});
socket.on("connect", () => {
socket.emit("join", { room: "support" });
});
socket.on("message:new", (msg) => {
console.log(`[${msg.room}] ${msg.from}: ${msg.text}`);
});With the default transports the first request is a polling handshake, which the endpoint answers with a 400 and a message telling you to add transports: ["websocket"]. Namespaces work (io("https://app.hooklistener.com/admin", …)), and binary attachments are recorded with the bytes inlined as base64.
> Create a Socket.IO endpoint "support chat", start chat-client.js
with CHAT_PATH set to its socketio_path, wait until it joins the
support room, then emit a message:new event and check the client
prints it.Each emitted event is recorded with event_type set to the event name and the payload set to its argument (one argument is stored as-is; several are stored as a JSON array). Since contains searches the payload, the agent waits for the room name, not the event name:
wait_for_realtime_message({ "endpoint_id": "9d04…", "contains": "support", "timeout": 0 })
{
"status": "message_received",
"message": {
"session_id": "e81f…",
"direction": "incoming",
"opcode": "event",
"event_type": "join",
"payload": "{\"room\":\"support\"}",
"metadata": { "namespace": "/", "args_count": 1 }
}
}
send_realtime_message({
"endpoint_id": "9d04…",
"session_id": "e81f…",
"event_type": "message:new",
"payload": "{\"room\":\"support\",\"from\":\"agent\",\"text\":\"Hi, ticket {{random.int:1000:9999}}\"}"
})On the way out, event_type becomes the event name (default message) and a JSON payload is decoded into a single argument, so socket.on("message:new", (msg) => …) receives an object. A payload that isn't JSON arrives as a string. If the client emits with an acknowledgement callback, the endpoint acknowledges it immediately with no arguments; custom ack payloads are not supported.
Walkthrough 3: An MQTT Client That Subscribes and Publishes on Topics
With "protocol": "mqtt" the endpoint is an MQTT 3.1.1 sink over WebSocket at wss://app.hooklistener.com/mqtt/<slug>. MQTT.js uses protocol version 4 (3.1.1) by default and connects over WebSocket when the URL starts with wss://:
import mqtt from "mqtt";
const client = mqtt.connect(process.env.MQTT_URL, { clientId: "thermostat-42" });
client.on("connect", () => {
client.subscribe("devices/thermostat-42/commands", { qos: 1 });
client.publish(
"devices/thermostat-42/telemetry",
JSON.stringify({ temp: 21.5, setpoint: 21 }),
{ qos: 1 }
);
});
client.on("message", (topic, payload) => {
const cmd = JSON.parse(payload.toString());
if (cmd.setpoint) {
client.publish(
"devices/thermostat-42/telemetry",
JSON.stringify({ temp: 21.5, setpoint: cmd.setpoint })
);
}
});The endpoint replies to CONNECT with CONNACK, to SUBSCRIBE with SUBACK (granted QoS is capped at 1), to PINGREQ with PINGRESP, and acknowledges QoS 1 publishes with PUBACK and QoS 2 with PUBREC/PUBCOMP. Every PUBLISH is recorded as an incoming message with event_type set to the topic and qos, retain and dup in its metadata. MQTT 5 clients are refused with “unacceptable protocol version”.
> Create an MQTT endpoint "thermostat", run thermostat.js with MQTT_URL
set to its connection URL, and check it subscribes to its commands
topic and publishes telemetry. Then send {"setpoint": 19} on the
commands topic and confirm the next telemetry reports setpoint 19.The SUBSCRIBE packet is recorded with the topic filters as its payload, so the agent can check the subscription itself before looking at telemetry:
wait_for_realtime_message({ "endpoint_id": "71ac…", "contains": "commands", "timeout": 0 })
→ event_type "SUBSCRIBE", payload [{"topic":"devices/thermostat-42/commands","qos":1}]
wait_for_realtime_message({ "endpoint_id": "71ac…", "contains": "temp", "timeout": 0 })
→ event_type "devices/thermostat-42/telemetry", payload {"temp":21.5,"setpoint":21},
metadata { "qos": 1, "retain": false, "dup": false, "packet_id": … }
send_realtime_message({
"endpoint_id": "71ac…",
"event_type": "devices/thermostat-42/commands",
"payload": "{\"setpoint\":19}"
})
wait_for_realtime_message({ "endpoint_id": "71ac…", "contains": "\"setpoint\":19", "timeout": 0 })
// then, if needed, again with "timeout": 30Messages you send are delivered as QoS 0 PUBLISH packets on the topic in event_type. Without one, the endpoint uses the client's first subscription, which is wrong if that is a wildcard filter such as devices/+/commands, so always pass the topic. The endpoint does not route messages between connected clients the way a broker does: what one client publishes is recorded, not forwarded to other subscribers.
Walkthrough 4: An SSE Client With Event Names, IDs and Reconnects
With "protocol": "sse" the endpoint serves GET https://app.hooklistener.com/sse/<slug> as a text/event-stream with CORS open to any origin, so a browser app on localhost can connect directly:
const url = import.meta.env.VITE_DEPLOY_EVENTS_URL;
const es = new EventSource(url);
es.addEventListener("deploy.progress", (e) => {
const { step, pct } = JSON.parse(e.data);
renderProgress(step, pct, e.lastEventId);
});
es.onerror = () => setStatus("reconnecting…");SSE is one-way, so there is no incoming message to wait for. Once the agent has opened the page (with its browser tool, or you have), it looks for the open session, then pushes events. If something else will connect the client later, wait_for_realtime_message with direction: "session" returns when it does.
list_realtime_sessions({ "endpoint_id": "3c55…", "status": "open" })
→ { "sessions": [ { "id": "a90b…", "protocol": "sse", "status": "open",
"origin": "http://localhost:5173", "request_path": "/sse/deploy-log-m2c5", … } ],
"count": 1 }
send_realtime_message({
"endpoint_id": "3c55…",
"event_type": "deploy.progress",
"event_id": "1",
"payload": "{\"step\":\"build\",\"pct\":40}"
})The client receives:
event: deploy.progress
id: 1
data: {"step":"build","pct":40}
An event_type of message (or none) omits the event: line, so it reaches onmessage; multi-line payloads become one data: line each. The stream sends a : keepalive comment every 30 seconds by default.
Testing reconnects with Last-Event-ID
Send events with numeric IDs 1, 2 and 3, then close the session from the console or with the REST API (POST /api/v1/realtime/endpoints/:id/sessions/:session_id/close). EventSource reconnects on its own and sends a Last-Event-ID header; the endpoint also accepts ?lastEventId=. On reconnect it replays the events the endpoint sent with a numeric ID greater than that value (up to 200), which is on by default. Push event 4 while the client is away and the agent can check it arrives exactly once after the reconnect. In the endpoint settings you can also send a retry: hint, change the keepalive interval, and have the endpoint number events automatically.
Auto-Responder Rules and Simulated Server Behaviour
Sending each reply by hand is fine for one exchange. For a client that expects a server to answer every request (a handshake, an auth message, a ping), let rules answer. manage_realtime_rules lists, creates, updates and deletes them. Rules run on every message a client sends (so not on SSE), in the order they were created, and the first match fires.
manage_realtime_rules({
"endpoint_id": "5b1e0c7a-…",
"action": "create",
"name": "confirm subscribe",
"match_mode": "json_path",
"conditions": "[{\"body.op\":{\"$eq\":\"subscribe\"}}]",
"rule_action": "reply",
"response_payload": "{\"type\":\"subscribed\",\"channel\":\"{{message.channel}}\",\"sid\":\"{{uuid}}\"}",
"delay_ms": 50
})- Match modes:
any,contains(substring),regex,json_path(filter conditions overbody, the parsed JSON payload, pluspayload,eventandopcode) andevent(the Socket.IO event name or MQTT topic, case-insensitive). - Actions:
replyto the sender,echothe message back,broadcastto every open session, orclosethe connection.response_event_typesets the reply's event name or topic, anddelay_ms(up to 30,000) delays it. - Template variables in replies, sends and on-connect payloads:
{{uuid}},{{now}}(ISO 8601),{{timestamp}}(ms),{{unix}},{{seq}}(per-session counter),{{random.int:MIN:MAX}},{{random.float}},{{random.hex:N}},{{random.pick:a|b|c}},{{message}}and{{message.path.to.field}}(the triggering message),{{event}},{{session.id}}and{{endpoint.slug}}. Unknown variables are left as written.
Two options on create_realtime_endpoint help here as well: on_connect_payload is sent to every client right after it connects (a welcome or hello message), and upstream_url turns the endpoint into a recording proxy in front of your real server (a public URL): each client session gets its own upstream connection, frames are relayed both ways, and everything is recorded.
Testing failure paths
- Malformed or unexpected messages: send invalid JSON, an unknown
type, a missing field or a binary frame withsend_realtime_messageand ask the agent whether the client logged an error, kept the connection and ignored the frame. - Server-side disconnects: a rule with
rule_action: "close"closes the connection when a message matches (for example a subscribe to a channel the user may not read), so the agent can check your reconnect and backoff logic. Over MCP the close code is 1000; the console and REST API let you choose any code from 1000 to 4999. - Auth failures: create the endpoint with an
auth_token. Clients must then present it as?token=(or?access_token=), anX-Hooklistener-TokenorAuthorization: Bearerheader, or, from a browser WebSocket, the subprotocolhooklistener-token.<token>. Without it the handshake fails with HTTP 401, which is how you test the client's error path. - Bad networks: the console and REST API (
PUT /api/v1/realtime/endpoints/:id/simulation) add outbound latency (up to 30 s) and jitter, drop or duplicate a percentage of messages, delay incoming messages, and close the connection after a percentage of client messages with a chosen close code. These settings are not exposed as MCP tools, so set them once and let the agent observe the result.
The console also has scenarios (scripted sequences of sends, waits, pings and closes, which can loop), expectations (“a message matching X must arrive within N ms of connecting”, checked per session) and JSON Schema validation that marks every message with schema_valid and schema_errors in its metadata.
Using It Without an Agent: Console and REST API
Every endpoint the agent creates also shows up in the console at app.hooklistener.com/realtime (the console_url in the reply): live sessions and messages, a composer to send to one session or all, rules, scenarios, expectations, settings, per-session export (JSON or NDJSON) and public share links that expire after 7 days by default (30 at most).
For CI, the same operations are under https://app.hooklistener.com/api/v1/realtime/, authenticated with a Hooklistener API key (hklst_…, paid plans) as a Bearer token: create, update and delete endpoints; list sessions; read a session's messages; search messages across an endpoint; send to a session or broadcast; close sessions; manage rules, scenarios and expectations; and tail events as SSE from GET /realtime/endpoints/:id/stream. There is no long-poll wait in the REST API, so a script polls the search endpoint:
API=https://app.hooklistener.com/api/v1
AUTH="authorization: Bearer $HOOKLISTENER_API_KEY"
EP=$(curl -sf -X POST "$API/realtime/endpoints" -H "$AUTH" \
-H 'content-type: application/json' \
-d "{\"name\": \"ci-orders-$GITHUB_RUN_ID\", \"protocol\": \"websocket\"}")
EP_ID=$(echo "$EP" | jq -r .data.id)
export ORDERS_WS_URL=$(echo "$EP" | jq -r .data.connection_url)
trap 'curl -sf -X DELETE "$API/realtime/endpoints/$EP_ID" -H "$AUTH"' EXIT
node orders-client.js > client.log &
found() { # $1 = text to look for in what the client sent
curl -sf -G "$API/realtime/endpoints/$EP_ID/messages/search" -H "$AUTH" \
--data-urlencode "q=$1" -d direction=incoming | jq -e '.data | length > 0' > /dev/null
}
for i in $(seq 1 20); do found '"op":"subscribe"' && break; sleep 1; done
found '"op":"subscribe"' || { echo "client never subscribed"; exit 1; }
curl -sf -X POST "$API/realtime/endpoints/$EP_ID/broadcast" -H "$AUTH" \
-H 'content-type: application/json' \
-d '{"payload": {"type": "order.created", "id": "ord_ci1", "total": 4200}}'
for i in $(seq 1 10); do found '"id":"ord_ci1"' && break; sleep 1; done
found '"id":"ord_ci1"' || { echo "client never acked"; exit 1; }payload can be a string or a JSON object; the send and broadcast bodies also take event_type, event_id, retry_ms (SSE) and opcode: "binary" with a base64 payload. The script deletes its endpoint on exit because endpoints count against your workspace limit.
Compared With Postman, wscat, websocat, Echo Servers, HiveMQ and Mockoon
Most real-time testing tools are clients: they connect to your server. Testing a client needs the opposite, a server the test controls. Here is what people use for that, checked September 2026 against each tool's own docs (sources below). We found no other tool that gives an AI agent hosted WebSocket, MQTT or SSE endpoints through an MCP server.
| Tool | What it is | Best at |
|---|---|---|
| Hooklistener real-time endpoints | Hosted server-side endpoints (WebSocket, Socket.IO, MQTT over WebSocket, SSE) with an MCP server and REST API | Agent-driven client tests: waits, scripted replies, recorded sessions |
| Postman | Client for WebSocket, Socket.IO and MQTT requests | Exploring a server by hand: connect, send, listen to events |
| wscat / websocat | Command-line WebSocket clients that can also listen as a local server | Quick local checks with no account; scripting in a shell |
| echo.websocket.org | Public WebSocket and SSE echo service | Checking that a client can connect at all |
| HiveMQ public broker | Shared public MQTT broker (TCP and WebSocket) | Real broker behaviour: routing between clients, QoS delivery |
| Mockoon | Open-source mock server (desktop app, CLI, Mockoon Cloud) with WebSocket routes | Mocking WebSocket and HTTP APIs together, offline |
Postman
Postman has WebSocket, Socket.IO and MQTT requests: you connect to a ws:// or wss:// server (or an MQTT broker), send messages and listen to events. It is a client, so it tests servers, not clients. Where it is better: exploring and documenting a server's real-time API by hand, alongside your HTTP collections.
wscat and websocat
Both are command-line WebSocket clients that can also listen as a server: wscat -l <port> and websocat -s <port>. That is the quickest way to give a local client something to talk to, with no account and no network. The server runs on your machine, though, so a phone, device or CI runner elsewhere can't reach it without a tunnel, nothing is recorded after the terminal closes, and there is no Socket.IO, MQTT or SSE. Where they are better: a fast local check, and scripting in a shell.
Public echo servers
echo.websocket.org echoes WebSocket messages and has an SSE endpoint. Echo proves a client can connect and send, but the server only repeats what it receives, so you can't script replies or push the event your client should handle.
HiveMQ public MQTT broker
broker.hivemq.com is a real, shared broker: TCP on 1883 (8883 with TLS) and WebSocket on 8000 (8884 with TLS). HiveMQ notes it is public and shared, so not for private or production data. Where it is better: anything that needs a broker's behaviour, such as several clients exchanging messages, retained messages, QoS delivery to subscribers and plain TCP clients. Hooklistener's MQTT endpoint is a recorded single-client sink for checking what one client publishes and how it reacts to commands; for load or routing tests, use a real broker.
Mockoon
Mockoon (open-source desktop app and CLI, plus Mockoon Cloud) has WebSocket routes since v9 with conversational and streaming modes and templating. Its docs say it does not implement the Socket.IO protocol, and its WebSocket docs don't cover MQTT or SSE. Where it is better: mocking a whole API, HTTP and WebSocket together, offline and versioned in your repo.
Sources, checked September 26, 2026: Postman WebSocket, Postman Socket.IO, Postman MQTT, wscat, websocat, echo.websocket.org, HiveMQ public broker, HiveMQ broker ports, Mockoon WebSockets, MQTT.js, Socket.IO client options.
Limits and What Is Not Supported
- Endpoints: 3 real-time endpoints per workspace by default. At the limit,
create_realtime_endpointreturns a plan-limit error with an upgrade link; deleting an endpoint frees a slot. MCP access itself is on every plan. - Retention: sessions and messages follow the plan's webhook history: 1 day on Free, 14 on Pro, 30 on Production, 90 on Scale. Pinned sessions stay, but their messages older than the window are still deleted.
- Connections and sizes: up to 200 open sessions per endpoint; frames up to 16 MB; each stored message keeps up to 128 KB of payload (larger ones are marked
truncatedwith the original size); a WebSocket with no traffic for 2 hours is closed. - Socket.IO: WebSocket transport only (no long-polling), socket.io-client 3.x/4.x protocol, acknowledgements answered with no arguments.
- MQTT: 3.1.1 over WebSocket only (no MQTT 5, no raw TCP), outbound messages at QoS 0, granted subscriptions capped at QoS 1, no routing between clients and no retained-message store.
- SSE: GET only and one-way, so rules and incoming waits don't apply; wait for
sessionoroutgoinginstead. - MCP tools: waits block for 60 seconds at most; network simulation, scenarios, expectations, schema validation and closing a session are set in the console or REST API, not through MCP. A read-only OAuth sign-in can list, wait and read but not create endpoints, send or manage rules.
- CLI tunnel:
hooklistener tunnelforwards HTTP only and rejects WebSocket upgrades, so it can't expose a local WebSocket server. Use a hosted endpoint, or proxy mode with a publicupstream_url.
FAQ
What URL does my client connect to?
It depends on the protocol you pick in create_realtime_endpoint. WebSocket: wss://app.hooklistener.com/ws/<slug>. MQTT over WebSocket: wss://app.hooklistener.com/mqtt/<slug>. SSE: https://app.hooklistener.com/sse/<slug>. Socket.IO: connect to https://app.hooklistener.com with the option path: "/sio/<slug>" and transports: ["websocket"]. The reply's connection_url (and socketio_path for Socket.IO) has the exact values.
Is the MQTT endpoint a real broker?
No. It is a single-client MQTT 3.1.1 sink over WebSocket. It answers CONNECT, SUBSCRIBE, UNSUBSCRIBE, PINGREQ and QoS 1 and 2 publishes the way a broker would, records every PUBLISH with its topic and QoS, and lets you publish to the client. It does not route messages between clients, keep retained messages or accept MQTT 5 or raw TCP connections. For broker behaviour, use a real broker.
Does wait_for_realtime_message return a task or block?
It blocks. The call holds open until a matching message or connection arrives or the timeout expires (default 30 seconds, maximum 60), then returns status message_received, session_connected or timeout. It only sees events that happen after it starts; with timeout: 0 it instead returns the newest matching message already recorded.
How long are sessions and messages kept, and how many endpoints can I have?
Sessions and messages follow your plan's webhook history: 1 day on Free, 14 days on Pro, 30 on Production and 90 on Scale, then they are deleted. A workspace can have 3 real-time endpoints by default; when you reach the limit, create_realtime_endpoint returns a plan-limit error with an upgrade link, or you can delete an old endpoint.
Can I test against my real server instead of a simulated one?
Yes. Pass upstream_url (a public ws://, wss://, http:// or https:// URL) when you create the endpoint. Each client session then opens its own connection to your server and every frame is relayed and recorded in both directions. The upstream has to be reachable from the internet.
Can I use the Hooklistener CLI tunnel for my local WebSocket server?
No. The CLI tunnel forwards HTTP requests and rejects WebSocket upgrade requests. To test a real-time client, point it at a hosted endpoint as this guide shows; to watch traffic to your own server, use proxy mode with a public upstream_url.
Related Reading
- The Hooklistener MCP server — all 67 tools, toolsets and client setup.
- WebSocket debugger — the console for real-time endpoints: sessions, network simulation and auto-responses.
- Test Signup, Magic-Link and OTP Emails With AI Agents — the same agent loop for the emails your app sends.
- Agentic webhook testing — wait-then-inspect for webhooks.
Give Your Agent a Server to Talk To
Create a free account at app.hooklistener.com, connect your agent with the steps on the MCP page, and ask it to create a real-time endpoint for your client's protocol. The free plan is enough to run every walkthrough on this page.