# Proposal: a `gmcp:` URI for out-of-band OSC 8 actions

Status: draft for discussion; experimental reference implementation in
PetriaMud WebUI and WebUI2

Author: Sammer from PetriaMud

Date: 2026-09-01

Copyright (c) 2026 Sammer for PetriaMud. All rights reserved.

## Summary

Mudlet's OSC 8 extensions provide rich, stateful controls, but an actionable
link can currently return information to the game only through `send:` or
`prompt:`. This proposal adds a `gmcp:` URI for an explicit user activation to
send one structured GMCP message without inserting or echoing a command in the
normal text stream.

The transport boundary becomes:

```text
info:    = the base interaction ends entirely in the client
send:    = the MUD receives player input in the command stream
prompt:  = the input line is prepared for player review
gmcp:    = the MUD receives structured data over negotiated GMCP
```

This is a companion to PetriaMud's
[`info:` URI proposal](osc8_info_proposal.md). Neither scheme replaces the
other: `info:` guarantees no network effect, while `gmcp:` is deliberately an
out-of-band action.

## Transport versus local behavior

The URI scheme selects exactly one activation transport. The OSC 8
configuration independently selects local presentation and state behavior.
They are orthogonal:

| Scheme | Primary activation result | May also use local OSC 8 configuration |
| --- | --- | --- |
| `info:` | no server message | yes |
| `send:` | command-stream input | yes |
| `prompt:` | prepared input line | yes |
| `gmcp:` | one structured GMCP message | yes |

Consequently, a `gmcp:` item may toggle a radio or checkbox, change its local
styling, participate in a menu, or use visibility behavior just as an `info:`
item can. It remains a `gmcp:` action because activation also sends a message
to the server. An `info:` item never sends GMCP and therefore retains a simple,
testable privacy and accessibility guarantee.

`send:` and `gmcp:` belong to the same family of server-facing actions, but
they expose different interfaces: `send:` is a textual command API and `gmcp:`
is a structured event API. Version 1 treats them as alternatives rather than
performing both for one activation.

## Motivation

The [Mudlet OSC 8 documentation](https://wiki.mudlet.org/w/Manual:Supported_Protocols#OSC_8:_Hyperlink_Protocol)
defines selection controls whose `send:` callback appends
`selected=true` or `selected=false`. That is appropriate when the game wants a
normal command, but it has drawbacks for structured UI events:

- the callback appears in the player's input channel;
- command parsing must recover structure from text;
- identifiers and values require custom quoting rules;
- a client cannot distinguish a game command from a UI event by transport;
- assistive or graphical interfaces may need to describe the action without
  reading a raw command.

GMCP already provides a separate channel for structured client/server data.
Mudlet exposes client-to-server messages through
[`sendGMCP()`](https://wiki.mudlet.org/w/Manual:Networking_Functions#sendGMCP),
but a game cannot currently attach that transport directly to a negotiated OSC
8 control without requiring a user script.

## Use cases where `send:` is not equivalent

Every example below could be represented by inventing a text command. That is
not equivalent behavior: `send:` enters the normal command channel and may
trigger command parsing, command logging, aliases, command feedback and a new
prompt. A screen reader may then announce output unrelated to the control that
the player just changed.

`gmcp:` is useful when the interaction itself should remain outside that text
conversation:

- **Accessible preference panels.** A player changes contrast, verbosity,
  sound or combat-hint preferences. The control updates locally and the server
  stores the structured value without printing or prompting again.
- **Radio choices in a multi-step form.** A player explores several ancestry,
  color or interface choices. Each radio change synchronizes draft state
  without advancing the command interpreter or filling the output window. A
  separate explicit action can commit the form.
- **Checkbox state shared across devices.** A setting is synchronized to the
  account while its checked state and visual response remain client-side. No
  synthetic command needs to be exposed to aliases or command history.
- **Structured menu choices.** Stable identifiers and typed values are sent
  independently from translated visible labels, without quoting them into a
  command or producing a prompt after every menu activation.
- **Quiet assistive interactions.** Repeated selection changes can be announced
  as state changes by the client instead of forcing a screen reader to consume
  command echoes, acknowledgements and repeated game prompts.
- **Virtual instruments.** A graphical piano can send a typed note identifier
  for each pressed key. The game validates and distributes the note as media,
  without representing the performance as a sequence of text commands.
- **Sound and media previewers.** A visual soundboard can request an allowlisted
  effect by stable identifier. The server answers through its negotiated media
  protocol while the normal output and prompt remain unchanged.
- **Rich visual interfaces.** Maps, palettes, configuration panels and other
  graphical controls can synchronize typed UI state without manufacturing
  hidden commands whose labels or quoting rules become part of the game API.

These are not requests to make meaningful gameplay actions invisible. Movement,
combat and other actions for which textual feedback is part of the game remain
better represented by `send:`.

### Example: a GMCP-driven piano

The proposed `gmcp:` action is client-to-server. It does not directly invoke
the server-to-client media package. A piano key could contain this OSC 8 URI:

```text
gmcp:Petria.Instrument.Note?data={"instrument":"piano","note":"C4","velocity":72}
```

Activation sends, outside the command stream:

```text
Petria.Instrument.Note {"instrument":"piano","note":"C4","velocity":72}
```

After validating the instrument and note, the game can answer the player and
nearby listeners with the standard server-to-client MUD Client Media Protocol
(MCMP) package:

```text
Client.Media.Play {"name":"piano/C4.ogg","type":"sound","tag":"piano","caption":"Piano note C4"}
```

No piano command is inserted, echoed or added to command history, and receipt
of the input event does not itself require a new textual prompt. The same model
supports a sound-test panel: the inbound message carries an allowlisted sound
identifier and the server maps it to a safe `Client.Media.Play` response. It
must not accept arbitrary client-provided file paths or URLs.

## Proposed URI

Version 1 uses this form:

```text
gmcp:Package.Name?data=<percent-encoded JSON object>&config=<OSC 8 config>
```

`data` is optional. `config` and `preset` keep their existing OSC 8 meanings.
Parameter order is not significant.

Example before percent-encoding:

```text
gmcp:Char.Preference.Set?data={"kind":"color","value":"red"}&config={"selection":{"group":"color","value":"red"}}
```

The same `data` payload in URI-safe form begins as:

```text
gmcp:Char.Preference.Set?data=%7B%22kind%22%3A%22color%22%2C%22value%22%3A%22red%22%7D&config=...
```

On activation, the client sends this GMCP message:

```text
Char.Preference.Set {"kind":"color","value":"red","selected":true}
```

The OSC 8 wire form remains unchanged:

```text
ESC ] 8 ;; gmcp:Char.Preference.Set?data=...&config=... ESC \
Red
ESC ] 8 ;; ESC \
```

### Why `gmcp:` rather than `sendgmcp:`

`gmcp:` names the protocol used on the wire and matches `http:`, `send:` and
`prompt:` as a URI scheme. `sendGMCP` is the name of Mudlet's Lua API and need
not be copied into an interoperable protocol understood by other clients.

### Why version 1 does not combine `send:` and `gmcp:`

Sending a command and a GMCP message for the same click creates two observable
server actions. A partially capable client, connection interruption or handler
failure could deliver only one of them. The protocol would also need to define
ordering, failure handling, duplicate suppression and whether either action is
allowed to proceed when the other is unavailable.

For version 1, producers choose one authoritative transport. If a game needs
the same internal behavior from both interfaces, its command handler and GMCP
handler can invoke the same server-side operation without requiring the client
to submit it twice.

A future negotiated extension could define an explicit ordered action list for
genuinely distinct compound operations. Such an extension should have its own
capability and atomicity rules; it should not silently attach GMCP to every
`send:` item.

## Parsing contract

Clients implementing version 1 MUST apply these rules:

1. The scheme is the exact ASCII string `gmcp:` in canonical output.
2. The package name is case-sensitive and consists of dot-separated
   identifiers. Each identifier begins with an ASCII letter and continues with
   ASCII letters, digits or `_`. The complete package name is at most 128
   bytes.
3. `data`, `config` and `preset` are reserved raw lowercase query names.
4. `data` is absent or a percent-encoded UTF-8 JSON object. Clients decode it
   exactly once. Arrays, strings, numbers, booleans and `null` are invalid as
   the top-level value in version 1.
5. Duplicate `data` parameters, malformed JSON, invalid UTF-8 or an invalid
   package name make the action non-executable. Visible text remains.
6. Unknown query parameters MUST NOT be copied into the GMCP message. A client
   may retain them for future protocol versions.
7. Existing OSC 8 sequence, configuration, nesting and size limits continue
   to apply. A `gmcp:` URI does not increase the 4096-byte OSC command limit.

Requiring a JSON object gives selection callbacks a deterministic place for
their state and avoids inventing different rules for array or scalar payloads.
A message with no payload remains valid when no selection callback is needed.

## Activation behavior

When the player directly activates a valid `gmcp:` item, the client MUST:

1. confirm that GMCP is currently negotiated for the connection;
2. construct the package and JSON payload as structured values;
3. send exactly one GMCP subnegotiation;
4. leave the command line and normal output stream unchanged.

Receiving the message does not constitute completion of a normal text-command
cycle. A server implementation SHOULD NOT emit its normal command prompt merely
because it received this GMCP action. The game remains free to send deliberate
textual or GMCP feedback when the action's semantics require it; therefore the
client cannot guarantee that the server will never produce subsequent output.

The implementation MUST NOT build Lua or other executable source code from the
URI. It should call the same internal structured sender used by `sendGMCP()`.

If GMCP is unavailable when the item is activated, the client MUST NOT fall
back automatically to sending the URI as text. It should perform no network
action and may provide accessible local feedback.

## Relationship with selection

Selection retains Mudlet's current local state model. The transport scheme
only determines how the directly activated item's callback reaches the game.

For `gmcp:` plus `selection`:

- the client toggles local radio or checkbox state first;
- it clones the base `data` object, or creates an empty object;
- it writes the top-level boolean `selected`, replacing any supplied value;
- it sends the resulting object in one GMCP message.

Only the item directly activated by the player emits a callback. Radio peers
deselected indirectly do not emit additional GMCP messages.

The existing `selection.group` and `selection.value` fields remain client-side
configuration and are not copied automatically. A producer includes any
server-required identity explicitly in `data`. This matches the current
`send:` rule, which adds selection state but does not serialize the full
selection configuration.

Example checkbox:

```text
gmcp:UI.Preference.Set?data={"preference":"combatHints"}
```

Selected activation:

```text
UI.Preference.Set {"preference":"combatHints","selected":true}
```

Deselected activation:

```text
UI.Preference.Set {"preference":"combatHints","selected":false}
```

As in Mudlet 5.0's current selection contract, a base item combining `menu`
and `selection` does not emit a selection callback. Choosing a menu entry
executes that entry according to its own URI scheme; a menu entry may itself
use `gmcp:`.

## Capability negotiation

A client advertises support through the NEW-ENVIRON `USERVAR`:

```text
OSC_HYPERLINKS_GMCP=1
```

The value means that the client recognizes `gmcp:`, validates it according to
this contract and can route activation through its GMCP sender. It does not
replace Telnet GMCP negotiation.

A server emits `gmcp:` only when all of these are true:

- OSC 8 is enabled and `OSC_HYPERLINKS=1`;
- `OSC_HYPERLINKS_GMCP=1` was explicitly received;
- Telnet GMCP option 201 is currently negotiated;
- every requested OSC 8 configuration feature is also confirmed.

Clients should report capability changes through NEW-ENVIRON `INFO`, following
the same dynamic update model used when OSC 8 is disabled in Mudlet 5.0.
Servers must not infer `gmcp:` support from a client name, version or GMCP
negotiation alone.

## Backward-compatible server behavior

Producers should choose a fallback based on semantics:

| Intended result | Preferred fallback without `gmcp:` |
| --- | --- |
| A safe equivalent command exists | `send:` with confirmed capabilities |
| Player should review the action | `prompt:` |
| The interaction is optional/local | `info:` or disabled presentation |
| No equivalent is safe | visible plain text |

A server MUST NOT hide a required gameplay action exclusively behind `gmcp:`.
The visible label must remain understandable when OSC 8 is ignored.

## Security considerations

- Activation requires an explicit player action. Rendering, hover, focus,
  visibility expiry and indirect radio deselection never send GMCP.
- Clients MUST allowlist the `gmcp:` scheme and MUST NOT pass unknown schemes
  to a platform URL handler.
- Package names and JSON are parsed as data, never interpolated into Lua.
- Every `Core.*` package MUST be rejected because session identity,
  capabilities, ping and transport keepalive are not player actions. Clients
  SHOULD reserve other internal namespaces and encourage game-owned action
  packages.
- Servers SHOULD maintain an explicit allowlist of packages that are safe as
  hyperlink actions. Registering a package in the normal GMCP dispatcher MUST
  NOT automatically make it available to server-authored OSC 8 controls.
- Each allowlisted server handler MUST validate its own closed payload schema,
  including duplicate and unknown fields, before producing an effect. A rate
  limit SHOULD count malformed attempts as well as successful actions.
- `data`, `config` and `preset` limits must be enforced before allocation or
  deep merge. Prototype-polluting keys must not affect client objects.
- A disabled item never sends GMCP and its context menu remains blocked under
  the existing OSC 8 rules.
- Clients SHOULD offer the same server-link safety setting or audit surface
  used for `send:` actions. Silent transport must not mean invisible consent.

## Accessibility

A `gmcp:` item is actionable and must not be exposed as passive `info:` text.
Clients should provide keyboard activation and announce a human-readable
tooltip or label rather than the raw package or JSON payload.

Selection continues to use radio or checkbox semantics with checked state.
Other `gmcp:` actions should use the client's established actionable-link or
button semantics. If GMCP becomes unavailable, the item should expose disabled
state and optional concise feedback instead of silently behaving like an
enabled control.

## Suggested conformance cases

1. Plain `gmcp:` activation sends one package and no command-line bytes.
2. A JSON object containing UTF-8, `&`, `#` and quotes survives one decode.
3. Radio and checkbox activations overwrite `selected` with a JSON boolean.
4. Indirect radio deselection sends no second callback.
5. `menu` plus selection sends only the chosen menu entry's action.
6. Missing GMCP negotiation, missing capability or disabled OSC 8 produces no
   GMCP traffic and leaves readable text.
7. Invalid package names, duplicate `data`, scalar JSON and overlong sequences
   remain non-executable.
8. Every `Core.*` package is rejected.
9. Keyboard and screen-reader users receive accurate role, state and label.
10. NEW-ENVIRON `INFO` changes are honored during the same connection.

## Non-goals

- This proposal does not define game-specific GMCP packages.
- It does not change normal GMCP negotiation or `Core.Supports`.
- It does not make `info:` capable of network callbacks.
- It does not make one activation send both command-stream input and GMCP.
- It does not transmit selection state automatically for `send:` or `prompt:`
  beyond Mudlet's existing behavior.
- It does not permit automatic activation or server-triggered client-to-server
  GMCP outside a player's action.

## Adoption request

We invite Mudlet and other MUD client developers to review the scheme name,
JSON-object restriction, selection merge rule, control-package denylist and
`OSC_HYPERLINKS_GMCP` capability. Feedback on whether a separately negotiated
compound-action extension would solve real use cases is also welcome, but it
is intentionally outside version 1. The intended guarantee is narrow:

> `gmcp:` represents one explicit, structured, out-of-band player action; it
> never becomes command text and it is never inferred from generic GMCP
> support.

## References

- [Mudlet supported protocols: GMCP](https://wiki.mudlet.org/w/Manual:Supported_Protocols#GMCP)
- [Mudlet OSC 8 hyperlink protocol](https://wiki.mudlet.org/w/Manual:Supported_Protocols#OSC_8:_Hyperlink_Protocol)
- [Mudlet `sendGMCP()` documentation](https://wiki.mudlet.org/w/Manual:Networking_Functions#sendGMCP)
- [MUD Client Media Protocol (MCMP)](https://wiki.mudlet.org/w/Standards:MUD_Client_Media_Protocol)
- [RFC 1572: Telnet Environment Option](https://www.rfc-editor.org/rfc/rfc1572)
- [OSC 8 hyperlinks](https://github.com/Alhadis/OSC8-Adoption)
