GMCP with Mudlet
GMCP (Generic Mud Communication Protocol) enables structured data exchange between MUD servers and modern clients like Mudlet through telnet option 201. Unlike raw text parsing, GMCP sends JSON payloads out-of-band, allowing clients to update UI elements, maps, and vitals without regex scraping. This guide assumes a C-based or similar socket-driven server architecture and covers the complete implementation from initial telnet negotiation to production debugging.

Enable Telnet Option 201 Negotiation
GMCP operates over telnet option 201 (0xC9). Upon client connection, send IAC WILL GMCP (0xFF 0xFB 0xC9) to advertise support. Wait for client response: IAC DO GMCP (0xFF 0xFD 0xC9) confirms the client handles GMCP. Store this state per socket descriptor.
// Send WILL GMCP to client
unsigned char will_gmcp[] = {0xFF, 0xFB, 0xC9};
send(client_fd, will_gmcp, 3, 0);
// In your telnet processor, detect DO GMCP
if (buf[i] == 0xFF && buf[i+1] == 0xFD && buf[i+2] == 0xC9) {
client->gmcp_enabled = 1;
i += 3; // Skip processed bytes
}⚠ Common Pitfalls
- •Sending GMCP payloads before receiving IAC DO GMCP breaks legacy clients
- •Failing to escape 0xFF bytes in JSON payloads causes premature sequence termination
- •Not tracking per-client GMCP state results in sending binary data to non-supporting clients
Implement Subnegotiation Frame Handling
GMCP data travels in telnet subnegotiation frames: IAC SB GMCP <payload> IAC SE (0xFF 0xFA 0xC9 ... 0xFF 0xF0). Extract the payload between these markers. Any 0xFF in the JSON must be doubled (0xFF 0xFF) per telnet rules to distinguish from IAC commands.
// Detect SB GMCP start
if (buf[i] == 0xFF && buf[i+1] == 0xFA && buf[i+2] == 0xC9) {
i += 3;
int start = i;
// Find IAC SE
while (i < len - 1 && !(buf[i] == 0xFF && buf[i+1] == 0xF0)) {
i++;
}
// Extract payload, handling doubled 0xFF
process_gmcp_payload(buf + start, i - start, client);
}⚠ Common Pitfalls
- •Infinite loops if IAC SE never arrives (implement timeouts)
- •Forgetting to unescape 0xFF 0xFF back to single 0xFF in JSON
- •Buffer overruns on malformed sequences without bounds checking
Parse Core.Supports for Capability Discovery
Upon GMCP enable, Mudlet sends Core.Supports with an array of supported packages (e.g., ["Char 1", "Room 2", "Map 1"]). Parse this to determine which modules the client accepts. Store capabilities per session to avoid sending unsupported packages.
// Example payload: Core.Supports.Set ["Char 1", "Room 2"]
void handle_gmcp(char *data, client_t *c) {
if (strncmp(data, "Core.Supports.Set ", 18) == 0) {
char *json = data + 18;
// Parse JSON array of "Module version" strings
c->gmcp_caps = parse_supports_json(json);
}
}⚠ Common Pitfalls
- •Assuming all clients support all packages breaks UI elements
- •Not handling Core.Supports.Add vs Core.Supports.Set distinction
- •Version string comparison errors (1 vs 1.0 vs 1.0.0)
Structure GMCP Outbound Payloads
Send data using format: Module.Action JSON_Object. Common modules include Char.Vitals, Room.Info, and Comm.Channel. Prefix with IAC SB GMCP, append IAC SE. Ensure JSON contains no newlines (single line only).
void send_gmcp(client_t *c, const char *module, const char *json) {
if (!c->gmcp_enabled) return;
char buf[MAX_LEN];
int len = snprintf(buf, sizeof(buf),
"%c%c%c%s %s%c%c",
0xFF, 0xFA, 0xC9, // IAC SB GMCP
module, json,
0xFF, 0xF0); // IAC SE
// Escape 0xFF in JSON content
len = escape_iac(buf, len);
send(c->fd, buf, len, 0);
}⚠ Common Pitfalls
- •Including newlines in JSON breaks telnet framing
- •Exceeding MTU size with large payloads (keep under 1024 bytes)
- •Forgetting to check gmcp_enabled flag before sending
Implement Core.Hello Handshake
Immediately after client confirms GMCP with IAC DO, send Core.Hello with server identification: {"client":"YourMUDName","version":"1.0"}. This identifies your server to client scripts. Wait for client Core.Hello response to confirm bi-directional communication.
// Send immediately after IAC DO GMCP received
send_gmcp(client, "Core.Hello", "{\"client\":\"MyMUD\",\"version\":\"2.4\"}");
// Handle incoming client hello
if (strstr(data, "Core.Hello")) {
// Extract client info for logging
log_client_capabilities(client);
}⚠ Common Pitfalls
- •Sending before client ready causes dropped packets
- •Not validating client version strings for compatibility
- •Missing Core.Hello triggers Mudlet 'Unknown GMCP' warnings
Transmit Char.Vitals for Status Bars
Send HP/MP/Mana updates whenever values change, not every tick. Format: Char.Vitals {"hp":"120","maxhp":"150","mp":"50","maxmp":"100"}. Mudlet maps these to gauges automatically if named consistently.
void update_vitals(character_t *ch) {
if (!ch->client->gmcp_enabled) return;
char json[256];
snprintf(json, sizeof(json),
"{\"hp\":\"%d\",\"maxhp\":\"%d\",\"mp\":\"%d\",\"maxmp\":\"%d\"}",
ch->hp, ch->max_hp, ch->mp, ch->max_mp);
send_gmcp(ch->client, "Char.Vitals", json);
}⚠ Common Pitfalls
- •Sending as integers vs strings (Mudlet expects strings per GMCP spec)
- •Flooding client with updates every combat round instead of on-change
- •Mismatching key names (hp vs health) breaks default Mudlet gauges
Send Room.Info for Mapper Integration
On every room change, send Room.Info with room ID, name, area, and exits. Mudlet's mapper requires consistent room IDs (numbers) and environment type. Exits should map direction to room ID or -1 for unknown.
void send_room_info(client_t *c, room_t *room) {
char json[512];
snprintf(json, sizeof(json),
"{\"num\":%d,\"name\":\"%s\",\"area\":\"%s\",\"environment\":\"%s\","
"\"exits\":{\"n\":%d,\"s\":%d,\"e\":%d,\"w\":%d}}",
room->id, json_escape(room->name), json_escape(room->area),
room->terrain,
room->exits[NORTH], room->exits[SOUTH],
room->exits[EAST], room->exits[WEST]);
send_gmcp(c, "Room.Info", json);
}⚠ Common Pitfalls
- •Room ID 0 breaks Mudlet mapper (use 1-based indexing)
- •Omitting exits object causes map disconnections
- •Special characters in room names without JSON escaping corrupt payload
Configure Mudlet Client Triggers
On the client side, create Lua scripts to handle incoming GMCP. Use `registerAnonymousEventHandler("gmcp.Char.Vitals", "updateVitals")` to capture server data. Store data in Mudlet tables for UI updates via Geyser gauges.
-- In Mudlet script editor
function onGMCP(event, args)
if event == "gmcp.Char.Vitals" then
local vitals = gmcp.Char.Vitals
hpBar:setValue(tonumber(vitals.hp), tonumber(vitals.maxhp))
elseif event == "gmcp.Room.Info" then
mapWidget:echo(gmcp.Room.Info.name)
end
end
registerAnonymousEventHandler("gmcp.Char.Vitals", "onGMCP")
registerAnonymousEventHandler("gmcp.Room.Info", "onGMCP")⚠ Common Pitfalls
- •Using registerTimer instead of registerAnonymousEventHandler for GMCP events
- •Assuming gmcp global exists before Core.Hello completes
- •Not converting string numbers to integers for gauge updates
Debug with Packet Inspection
Use tcpdump or Wireshark to verify telnet sequences: `sudo tcpdump -i lo port 4000 -X -vv | grep -A2 "ff fa c9"`. Look for IAC SB GMCP (ff fa c9) followed by readable JSON and IAC SE (ff f0). Check for doubled 0xFF sequences in payloads.
# Capture and filter GMCP packets
tcpdump -i any port 4000 -X -n | tee gmcp.log &
# In another terminal, connect with Mudlet
# Then grep for GMCP sequences:
grep "ff fa c9" gmcp.log
# Verify JSON integrity:
grep -oP '(?<=ff fa c9 ).*(?= ff f0)' gmcp.log | jq .⚠ Common Pitfalls
- •TLS encryption prevents tcpdump visibility (disable for testing)
- •Small MTU fragmentation splitting IAC sequences across packets
- •Wireshark telnet dissector not decoding option 201 as GMCP specifically
Maintain Backward Compatibility
Always check `gmcp_enabled` flag before sending sequences. For non-GMCP clients, continue sending traditional prompt lines with HP/MP status. Never send IAC sequences to clients that haven't negotiated GMCP, as this corrupts their display with binary garbage.
void send_status(character_t *ch) {
if (ch->client->gmcp_enabled) {
send_gmcp(ch->client, "Char.Vitals", build_vitals_json(ch));
} else {
// Traditional prompt for legacy clients
send_text(ch->client, "HP:%d/%d MP:%d/%d> ",
ch->hp, ch->max_hp, ch->mp, ch->max_mp);
}
}⚠ Common Pitfalls
- •Accidentally sending GMCP to Telnet clients showing binary garbage
- •Removing text prompts entirely (some GMCP users still want text backup)
- •Not handling clients that negotiate GMCP but don't implement specific modules
What you built
GMCP implementation requires strict adherence to telnet framing rules and JSON structure, but provides robust out-of-band communication for modern MUD clients. Test thoroughly with both Mudlet and standard telnet to ensure graceful degradation. Monitor server logs for malformed JSON errors and client disconnections during negotiation phases. Once stable, expand beyond Core and Char modules to implement custom packages for your specific MUD mechanics.