Skip to content

MPP Protocol Reference

  • Version: MPP v2
  • Audience: Developers implementing a mobrule Pack Adapter in any language.

Breaking change from v1. MPP v2 introduces a mandatory pull{queue} inbound frame as the credit grant for dispatch, adds a queue routing tag to the outbound invocation frame, narrows the disconnect-requeue scope to in-flight rows only, and rejects legacy mpp:1 envelopes with protocol_error code=INVALID_FRAME. There is no fallback path: adapters that do not send pull cannot pump invocations; the bridge does not eagerly dispatch on row insert. MPP v1 is deprecated and unsupported.

The Mobrule Pack Protocol (MPP) is the wire format used between the mobrule bridge and a Pack’s Adapter process. The bridge is the platform-owned router that relays Invocations (Pack events that need to run inside the game). The Adapter is community-supplied: it speaks MPP and translates each Invocation into actual in-game effects.

The protocol is line-oriented JSON over TCP. The Adapter connects; the bridge listens. After a successful authentication handshake the Adapter must issue a pull{queue} for each queue it is ready to service; the bridge then dispatches at most one matching Invocation per outstanding pull, and the Adapter reports the outcome back. This document is the authoritative reference for the v2 wire format. Adapters written from this document alone are guaranteed to interoperate with any conforming bridge.

A complete session, from handshake to the per-credit dispatch loop:

  • Protocol: TCP. The bridge binds a port (default 7777, configurable via --port); the Adapter is the initiator.
  • Framing: NDJSON, exactly one JSON object per line. Each frame is terminated by a single LF byte (\n, 0x0A). CRLF is not emitted by either side; receivers SHOULD tolerate a trailing \r before the LF as a safety net but MUST NOT depend on it.
  • Encoding: UTF-8.
  • No length prefix. Parsers buffer until LF and parse the preceding bytes as a single JSON value.
  • Max frame size: 64 KiB (65,536 bytes including the trailing LF). If either side observes a line exceeding this cap, it MUST close the connection. The bridge SHOULD send a protocol_error with code = "FRAME_TOO_LARGE" before closing if at all possible.
  • Forward compatibility: parsers MUST tolerate unknown keys. Future minor versions of v2 may add optional fields; receivers ignore unrecognised keys silently.

Every frame, in either direction, MUST include:

{"mpp": 2, "type": "<frame-type>"}
  • "mpp" (integer) is the protocol major version. v2 implementations emit and accept only 2.
  • "type" (string) is the frame discriminator and selects how the remaining fields are interpreted.

JSON objects have no guaranteed key ordering. Senders SHOULD emit "mpp" and "type" first by convention, but parsers MUST tolerate any key order.

The Adapter’s hello MUST carry "mpp": 2. The mpp version is checked at frame-parse time, before the hello payload is interpreted. If the value is anything other than 2, the bridge replies with protocol_error code=INVALID_FRAME and closes the connection. There is no hello_ack for a version-rejected hello. Legacy mpp:1 envelopes are rejected by this same path: there is no compatibility shim.

Worked example, successful negotiation:

{"mpp": 2, "type": "hello", "pack_id": "com.example.mypack", "manifest_hash": "sha256:abc123...", "bridge_token": "<raw-token>"}
{"mpp": 2, "type": "hello_ack", "ok": true, "broadcaster_login": "streamer_login", "session_id": 0}

Worked example, rejected mpp:1:

{"mpp": 1, "type": "hello", "pack_id": "com.example.mypack", "manifest_hash": "sha256:abc123...", "bridge_token": "<raw-token>"}
{"mpp": 2, "type": "protocol_error", "code": "INVALID_FRAME", "message": "unsupported mpp version: Some(1)"}

Future versions may introduce a counter-proposal field in hello_ack to advertise a supported downgrade; v2 does not.

A→B denotes Adapter-to-Bridge; B→A denotes Bridge-to-Adapter.

FrameDirectionLifecycle phasePurpose
helloA→BauthAdapter self-registers
hello_ackB→AauthBridge confirms or rejects
pullA→BdispatchAdapter grants one credit on a queue
invocationB→AdispatchBridge sends an Event to the Adapter
ackA→BdispatchAdapter confirms receipt of invocation
appliedA→BdispatchAdapter reports effect applied in-game
doneA→BdispatchAdapter signals completion (terminal)
failedA→BdispatchAdapter reports failure (terminal)
configB→AdispatchBridge delivers a latched Pack-config value
heartbeatA→BkeepaliveLiveness ping; no response required
logA→BtelemetryAdapter emits a log line
state_writeA→BtelemetryAdapter writes an adapter-owned Pack-state key
protocol_errorB→AerrorBridge signals a protocol violation; some (e.g. bad version, malformed frame) close the connection, others (e.g. UNKNOWN_QUEUE) do not; see §5.12

The thirteen frame types listed above constitute the complete MPP v2 frame set.

In every example below the "mpp": 2 and "type" fields are mandatory; field tables document the additional payload.

{"mpp": 2, "type": "hello", "pack_id": "com.example.mypack", "manifest_hash": "sha256:abc123...", "bridge_token": "<raw-token>"}
FieldTypeRequiredNotes
pack_idstringyesReverse-DNS Pack identifier. MUST equal the bridge’s loaded Pack.
manifest_hashstringyesPack Manifest hash in the form "sha256:" + 64 lowercase hex.
bridge_tokenstringyesRaw bearer token issued out-of-band by the platform during pairing.

The Adapter MUST send hello as the first frame on a fresh connection. The bridge MUST receive hello within 10 seconds of accept; otherwise it closes the connection silently. A second hello on an already-authenticated connection is rejected with protocol_error code=INVALID_FRAME (“duplicate hello”) and the connection is closed.

{"mpp": 2, "type": "hello_ack", "ok": true, "broadcaster_login": "streamer_login", "session_id": 0}
FieldTypeRequiredNotes
okbooleanyestrue on success.
broadcaster_loginstringyes (ok)The Twitch broadcaster login the bridge is authenticated as.
session_idu64yes (ok)Currently always 0. STDB sessions are identity-keyed, not numbered; the field is a forward-compat placeholder for a future numbered-session scheme.
persistedarraynoReplay of durably stored persist Pack-state keys. Omitted when empty. See below.

On success the bridge server-pins pack_id and broadcaster_login from its loaded Manifest and session record. The Adapter is now considered the active Adapter for that session and MUST issue a pull for each queue it intends to service before any invocation will be sent.

persisted[] replay. For every Pack-state key declared persist = true in pack.toml that has a durably stored value, the bridge includes one { key, value, scope } row:

{"mpp": 2, "type": "hello_ack", "ok": true, "broadcaster_login": "streamer_login", "session_id": 0,
"persisted": [{"key": "resets", "value": 3, "scope": "trainer:42"}]}
FieldTypeRequiredNotes
keystringyesThe declared persist Pack-state key.
valueanyyesThe last durably stored value (schema-valid under the key’s current schema).
scopestringnoThe opaque run-scope tag the Adapter attached on the durable write. Omitted when none.

The bridge replays unconditionally. The bridge never interprets, compares, or parses scope: it stores it, echoes it here verbatim, and the Adapter alone matches each scope against its current run to decide resume-vs-reset. The Adapter MUST apply persisted[] before its first state_write, or it would clobber the seeded value with zeros under last-write-wins. A pack with no persist keys sees no persisted field at all.

{"mpp": 2, "type": "hello_ack", "ok": false, "reason": "manifest_hash_mismatch"}
FieldTypeRequiredNotes
okbooleanyesfalse on failure.
reasonstringyes (!ok)One of the reason codes below.

Reason codes:

  • invalid_token: bridge_token does not match.
  • manifest_hash_mismatch: Adapter’s manifest_hash differs from bridge’s.
  • pack_id_mismatch: Adapter claims a different pack_id.
  • unpaired: bridge has not completed its pairing handshake with the cloud.

A wrong mpp version does not produce a failure hello_ack: it is rejected earlier with protocol_error code=INVALID_FRAME (see §3.1).

After sending a failure hello_ack the bridge closes the TCP connection.

{"mpp": 2, "type": "invocation", "id": 123, "queue": "default", "event": "give_item", "params": {"item_id": 4, "count": 1}, "username": "ViewerName"}
FieldTypeRequiredNotes
idu64yesUnique Invocation identifier.
queuestringyesRouting tag identifying which Pack-declared queue this Invocation belongs to. Matches the [events.<name>].queue value in the Pack Manifest. The Adapter MUST route this Invocation to its internal per-queue handler.
eventstringyesEvent name as declared in the Pack Manifest.
paramsobjectyesEvent parameters; already JSON-Schema-validated by the bridge before dispatch. For an Event declaring viewer input, the bridge may inject the redeemer’s free-text input into one or more declared fields before validation, so treat params as viewer-influenced data rather than purely broadcaster/Pack-controlled.
usernamestringnoThe redeemer’s cased Twitch display name (never the lowercase login), for in-game attribution. Omitted when no name resolves (e.g. a manual invoke with no login).

When the bridge sends an invocation it has already transitioned the Invocation from pending to dispatched, and a matching pull{queue} credit has been consumed (see §5.5 and §6). The Adapter SHOULD respond promptly with at least one of ack, applied, done, or failed, and MUST wait until the queue’s configured ready_after state is reached before sending the next pull for the same queue.

{"mpp": 2, "type": "pull", "queue": "default"}
FieldTypeRequiredNotes
queuestringyesName of the queue the Adapter is ready to service. MUST be a queue declared in the active Pack Manifest.

Credit-of-1 semantics. Each pull{queue} grants the bridge permission to dispatch at most one matching Invocation on that queue. The bridge does not pre-credit and does not multi-pull. After the Adapter sends pull{queue} and the bridge dispatches a matching pending Invocation, the Adapter MUST wait until that Invocation reaches the queue’s ready_after state (applied by default, or done if [queues.<name>].ready_after = "done") before sending the next pull for that same queue. Sending an additional pull for a queue that already has an outstanding (parked) credit is tolerated: the bridge treats it as a benign refresh and re-parks the credit rather than rejecting it. Adapters SHOULD still send exactly one pull per completed Invocation on a queue; relying on the refresh behaviour is not a substitute for correct credit bookkeeping.

Per-queue independence. Pulls on different queues are independent: the Adapter MAY have one outstanding pull per declared queue simultaneously. This is how an Adapter pumps multiple queues concurrently while preserving strict serial ordering inside each queue.

Unknown queue. If queue is not declared in the active Pack Manifest, the bridge replies with protocol_error code="UNKNOWN_QUEUE" and does not dispatch, but the connection stays open and the dispatch loop keeps running (see §5.12).

No fallback. If the Adapter never sends pull{queue}, the bridge will never dispatch on that queue. Pending Invocations accumulate indefinitely. This is by design: there is no eager-dispatch escape hatch.

{"mpp": 2, "type": "ack", "id": 123}

The Adapter confirms it received the invocation frame. The bridge logs receipt; the Invocation’s state does not change (it remains dispatched). ack is informational and does NOT release the queue’s in-flight slot (see §6).

{"mpp": 2, "type": "applied", "id": 123, "result": {"resolved_item_id": 4, "resolved_count": 1}}
FieldTypeRequiredNotes
idu64yesThe Invocation id this frame relates to.
resultobject or nullyesEffect-specific result payload. May be null if there is nothing to report.

The bridge transitions the Invocation from dispatched to applied. If the queue’s ready_after = "applied" (the default), this also clears the queue’s in-flight slot, freeing it to dispatch the next parked or future pull for the queue (see §6).

{"mpp": 2, "type": "done", "id": 123}

The Adapter signals completion. The bridge transitions the Invocation from applied to done. done is terminal; no further frames for this id are expected or honoured. If the queue’s ready_after = "done", done clears the queue’s in-flight slot.

{"mpp": 2, "type": "failed", "id": 123, "reason": "item not found in inventory"}
{"mpp": 2, "type": "failed", "id": 124, "reason": "target already fainted", "refund": true}
FieldTypeRequiredNotes
idu64yesThe Invocation id this frame relates to.
reasonstringyesHuman-readable failure reason; recorded verbatim.
refundboolnoIf true, signals the originating Cue should be refunded to the viewer. Default false.

When refund is omitted or false, the bridge transitions the Invocation to terminal state failed.

refund=true reaches terminal state refunded only when both hold:

  1. The Invocation’s source is channel_points. For every other source (bits, subs, raids, manual) the bridge ignores refund and settles terminal failed: there is no upstream redemption to cancel.
  2. The Invocation has not yet been applied. While the Invocation is pre-applied (pending or dispatched), refund=true mints a refund request and settles refunded. After applied, the effect provably ran, so refund=true settles terminal failed instead. A held event that was dispatched but never applied (e.g. a battle event abandoned when the battle ended) is the case refund=true exists for.

On a refund the platform calls the upstream provider’s refund API (Twitch Helix UpdateRedemptionStatus set to CANCELED for channel-point redemptions), returning the viewer’s points.

failed, refunded (terminal) always clear the queue’s in-flight slot.

Adapters SHOULD use refund=true only when the Invocation could not be meaningfully applied (target already in requested state, prerequisite resource missing, gating impossible). Adapters MUST NOT use refund=true to express “applied, but partially”: that is applied followed by done or failed without refund.

{"mpp": 2, "type": "heartbeat"}

The Adapter MAY emit heartbeat at any time after the handshake. The bridge does not emit heartbeat (there is no outbound variant). No response is required. The bridge accepts and logs each heartbeat but does not currently track staleness or enforce any deadline on the MPP connection based on it.

{"mpp": 2, "type": "log", "level": "info", "message": "loaded map in 12 ms"}
FieldTypeRequiredNotes
levelstringyesOne of trace, debug, info, warn, error.
messagestringyesFree-form log line.

The bridge writes log frames to its own debug log; it does not retain them anywhere the Adapter or platform can query later.

{"mpp": 2, "type": "state_write", "key": "resets", "value": 3, "scope": "trainer:42"}
FieldTypeRequiredNotes
keystringyesA [state.<key>] declared in the active Pack Manifest.
valueobject/anyyesArbitrary JSON; validated against the key’s declared schema (if any).
scopestringnoOpaque run-scope tag. Stored + replayed for a persist = true key; ignored for non-persist keys. The bridge never interprets it.

The Adapter is the writer for a Pack-state key only when that key’s [state.<key>] writer = "adapter". On receipt the bridge enforces, in order:

  1. key is declared in the active Manifest’s [state.*] table, else protocol_error code=INVALID_FRAME and close.
  2. The key’s declared writer is adapter, else protocol_error code=INVALID_FRAME and close (the frame is structurally valid but the Adapter is not the authorised writer).
  3. value validates against the key’s compiled JSON Schema (when the key declares a schema; keys with no schema accept any JSON), else protocol_error code=SCHEMA_VIOLATION and close.

On success the bridge inserts the value into the Pack-state store keyed by (pack_id, key). There is no outbound acknowledgement frame: an accepted write is silent, and the snapshot/diff fan-out to overlay subscribers is the store’s concern, not this frame’s.

When the key is declared persist = true, the accepted value + scope are additionally durably stored (eventually durable: an async write, not commit-before-ack) keyed by (pack_id, key), last-write-wins, and replayed to the Adapter in the next hello_ack persisted[] leg (§5.2). The durable store survives both Adapter and bridge restarts.

{"mpp": 2, "type": "config", "key": "hardcore_mode", "value": {"enabled": true}}
FieldTypeRequiredNotes
keystringyesA [config.<key>] declared in the active Pack Manifest.
valueanyyesThe current broadcaster-edited value, schema-valid under the key’s declared [config.<key>].schema.

config carries the current broadcaster-edited value for one declared [config.<key>]. The bridge replays one config frame per declared key that has a known value immediately after hello_ack (after the persisted[] leg), and pushes a fresh config frame whenever the value changes (a broadcaster edit in the Dashboard). The value is schema-validated against the key’s declared schema before it is sent, so the Adapter only ever receives a valid value.

config is latched, not one-shot: the Adapter receives the current value on every connect and on every change, and may receive the same value more than once. The Adapter MUST apply each config value idempotently. There is no acknowledgement frame.

{"mpp": 2, "type": "protocol_error", "code": "INVALID_FRAME", "message": "unknown type: 'foo'"}
FieldTypeRequiredNotes
codestringyesMachine-readable error code (see list below).
messagestringyesHuman-readable detail.

Codes:

  • INVALID_FRAME: JSON parse failed, a non-2 mpp value, missing mpp/type, or unknown type. Also covers a duplicate hello on an already-authenticated connection, and a state_write to an undeclared key or to a key the Adapter does not own.
  • UNKNOWN_QUEUE: Adapter sent pull{queue} for a queue not declared in the active Pack Manifest.
  • SCHEMA_VIOLATION: params for an Invocation, or a state_write value, failed schema validation.
  • FRAME_TOO_LARGE: line exceeded 64 KiB.

An ack, applied, done, or failed referencing an id the bridge did not dispatch is not a protocol error: the bridge logs a warning, ignores the frame, and leaves the connection open.

Not every protocol_error closes the connection. Whether the connection closes depends on where the error originates:

  • Read-loop errors close the connection. A malformed frame or bad mpp version, a duplicate hello, a state_write that violates the declared key/writer/schema, and FRAME_TOO_LARGE are all detected in the frame read loop; the bridge sends protocol_error (when it can) and closes the TCP connection immediately.
  • Dispatch-side errors leave the connection open. UNKNOWN_QUEUE (from an errant pull) and a dispatch-time SCHEMA_VIOLATION (an Invocation’s params failing schema validation before it reaches the Adapter) are reported the same way but do not close the connection; the dispatch loop keeps running and the Adapter can keep pulling on its other queues.

An Adapter MUST treat any protocol_error as a serious diagnostic and log it, but the correct reaction (reconnect vs. carry on) depends on whether the underlying TCP connection is still open.

An Adapter-originated protocol_error (sent A→B in unusual recovery scenarios) is logged by the bridge and closes the connection.

MPP v2 is pull-driven. Dispatch occurs on a given queue if and only if both conditions hold:

  1. The Adapter has sent a pull{queue} whose credit is still outstanding (parked) for that queue.
  2. A pending Invocation exists whose queue equals that queue and whose pack_id + manifest_hash match the bridge’s server-pinned identity.

The bridge does not dispatch eagerly when an Invocation is created. A new pending Invocation whose queue has no parked pull simply waits; it will be picked up the next time the Adapter sends pull{queue}. There is no fallback path: Adapters that do not pull do not pump.

Each Invocation walks a fixed state machine. done, failed, and refunded are terminal; a mid-dispatch Adapter disconnect requeues the in-flight row from dispatched back to pending:

For each queue the bridge maintains an in-memory record of:

  • in_flight: set when an Invocation has been dispatched on this queue and not yet cleared; empty otherwise.
  • pending_pull: set when a pull{queue} has arrived for this queue and not yet been consumed by a dispatch; cleared on dispatch.
  • ready_after: resolved from the active Manifest’s [queues.<name>].ready_after at the time of the first pull.

A queue is dispatch-eligible iff in_flight is empty and pending_pull is set. When both conditions hold and a matching pending Invocation exists, the bridge dispatches the oldest such Invocation, marks it in-flight, and clears pending_pull.

[queues.<name>].ready_after controls when the bridge clears in_flight for that queue:

  • "applied" (default): in_flight clears as soon as the bridge receives applied for the in-flight Invocation. The queue immediately becomes dispatch-eligible if a new pull is parked, even though the Invocation may still progress to done or failed. Use this when the Adapter’s “applied” event represents the point at which the next Invocation can safely run.
  • "done": in_flight clears only when the bridge receives done (or failed) for the in-flight Invocation. The queue is held strictly until the terminal transition. Use this for queues whose effects must fully complete in-game before the next Invocation begins.

failed (terminal) always clears in_flight, regardless of ready_after.

Within a single queue, at most one Invocation is in flight at any time. The credit-of-1 + in-flight gate guarantees this without any extra coordination: while an Invocation is in flight, no subsequent pull on the same queue can be consumed.

Across queues, dispatches are independent. An Adapter with N declared queues can have up to N Invocations in flight simultaneously (one per queue).

Queue names are not validated when a Cue produces an Invocation; validation happens at bridge dispatch time:

  • If a pending Invocation names a queue that the bridge’s loaded Manifest does not declare, the bridge marks it failed with reason = "unknown_queue: <name>" and does not dispatch it.
  • If the Adapter sends pull{queue} for a queue the Manifest does not declare, the bridge replies with protocol_error code="UNKNOWN_QUEUE" and does not dispatch, but leaves the connection open.

After a successful hello_ack, the bridge records pack_id and broadcaster_login as the server-pinned identity for the connection. These values are taken from the bridge’s loaded Manifest and its platform session, not from anything the Adapter sends in subsequent frames.

The Adapter MUST NOT include pack_id or broadcaster_login in non-hello frames. If the bridge sees those keys in any non-hello frame, they are silently ignored. This rule prevents a compromised or buggy Adapter from claiming another Pack’s identity once connected.

Similarly, queue is a routing tag on invocation and pull only: the bridge does not honour queue on terminal frames (ack, applied, done, failed). The bridge resolves the queue for those frames from its own in-flight map keyed by the Invocation id.

When the Adapter’s TCP connection closes (clean FIN, RST, read error, or protocol_error), the bridge:

  1. Stops dispatching further Invocations to this Adapter.
  2. For every queue, requeues only the Invocation currently in flight (i.e. dispatched-but-not-yet-cleared) back to pending. Invocations that have already reached applied on a ready_after = "done" queue are left in applied: they are NOT requeued. This narrowed scope eliminates the double-application risk that the v1 “requeue everything dispatched” rule had for long-running ready_after = "done" workflows.
  3. Pending Invocations whose queue had a parked pull at disconnect time stay pending; the next Adapter connection’s first pull{queue} will pick them up.
  4. The bridge clears its in-memory per-queue state and resumes listening for a new Adapter connection. State is built fresh on the next hello.

Invocations belonging to other bridge sessions are untouched. There is no timeout-based retry. The bridge does not re-send or duplicate Invocations after dispatch. Resilience is provided exclusively by the narrowed re-queue-on-disconnect mechanism above.

Adapter Bridge
| ---- hello (mpp:2) -----> |
| <---- hello_ack (ok) ------- | bridge server-pins pack_id + broadcaster_login
| |
| ----- pull (queue=default) -> | bridge parks credit on "default"
| <--- invocation (id=42, | bridge: invocation 42 pending → dispatched
| queue=default) -------- | in_flight[default] = 42; pending_pull[default] = false
| ----- ack (id=42) ----------> |
| ---- applied (id=42) -------> | bridge: 42 dispatched → applied
| | ready_after=applied → in_flight[default] = None
| ------ done (id=42) --------> | bridge: 42 applied → done (terminal)
| ----- pull (queue=default) -> | bridge parks next credit
| |
| ----- heartbeat ------------> |
| ---- log (info, "...") -----> | bridge writes to its own debug log
| |
| [TCP close] | bridge requeues ONLY in-flight rows
| | per queue → pending

9.1 Invocation lifecycle (for cross-reference)

Section titled “9.1 Invocation lifecycle (for cross-reference)”
pending ──► dispatched ──► applied ──► done (terminal)
pending ──► dispatched ──► applied ──► failed (terminal; refund=true ignored post-applied)
pending ──► dispatched ──► failed (terminal; refund=false, or non-channel_points)
pending ──► dispatched ──► refunded (terminal; refund=true, channel_points, pre-applied)
pending ──► failed (validation-fail before dispatch, e.g. unknown_queue)

The bridge owns every transition. The Adapter never talks to the platform’s database directly. The refunded state is reached only via a failed frame carrying refund: true, and only when the Invocation is a channel_points source still pre-applied (§5.9). A refund=true on any other source, or after applied, settles terminal failed.

ScenarioAction
Inbound JSON parse errorBridge sends protocol_error code=INVALID_FRAME, closes connection.
Frame missing "mpp" or value ≠ 2 (hello or any later frame)Bridge sends protocol_error code=INVALID_FRAME, closes connection. The version is checked at parse time, before the payload, so the hello path is no exception.
pull{queue} referencing a queue not in the active ManifestBridge sends protocol_error code=UNKNOWN_QUEUE; connection stays open, dispatch loop keeps running.
ack/applied/done/failed referencing an id this connection did not dispatchBridge logs a warning, ignores the frame, leaves the connection open.
Bridge-side schema validation fails before dispatchBridge marks the Invocation failed with reason = "schema_validation: <details>" and sends protocol_error code=SCHEMA_VIOLATION; connection stays open. The Adapter never receives the invocation frame for this row.
Pending Invocation naming a queue not in the active ManifestBridge marks it failed with reason = "unknown_queue: <name>"; no invocation is emitted.
Duplicate hello on an already-authenticated connectionBridge sends protocol_error code=INVALID_FRAME, closes connection.
Adapter sends protocol_errorBridge logs the frame and closes the connection.
Frame exceeds 64 KiBReceiver closes the connection; bridge SHOULD send protocol_error code=FRAME_TOO_LARGE first.
state_write to a key not declared in the active Manifest, or to a key the Adapter does not own (writeradapter)Bridge sends protocol_error code=INVALID_FRAME, closes connection; no store write.
state_write value fails the key’s compiled schemaBridge sends protocol_error code=SCHEMA_VIOLATION, closes connection; no store write.

MPP v2 is designed for localhost deployment: the bridge binds to a local TCP port and the Adapter runs on the same machine. The bridge_token is a shared secret of approximately 32+ bytes. The following are explicit choices, not oversights:

  • No TLS: the assumption is loopback transport. Operators who expose the bridge port off-host are responsible for tunnelling it themselves.
  • Token comparison is a plain hash equality (sha256(received) == stored_hash). Constant-time comparison is not specified because the threat model precludes a meaningful timing-attack adversary.
  • The bridge ignores pack_id/broadcaster_login outside hello precisely to limit damage from a hostile Adapter.
  • Queue names are capped at 64 bytes at ingress and validated against the loaded Manifest at bridge dispatch; a hostile or buggy caller cannot inflate storage by injecting arbitrary queue names.
VersionStatusNotes
v2CurrentAdds pull{queue} credit grant + invocation.queue routing tag; narrows disconnect requeue to in-flight per queue; rejects mpp:1. Breaking; no v1 fallback.
v1DeprecatedInitial release. Eager dispatch; single-slot serial dispatch; no queue concept. Unsupported as of v2; mpp:1 envelopes are rejected with protocol_error code=INVALID_FRAME.