Browse documentation
DocsExtensionsV1

Protocol

v1 traffic is JSON-RPC 2.0 on a length-prefixed TCP socket. Kind 0 is JSON. Kind 1 is a binary attachment, used today for framebuffer pixels.

Frames

Every message:

  1. 4-byte big-endian payload length
  2. payload: 1 kind byte, then the body

Kind 0 body is a UTF-8 JSON-RPC object. Kind 1 body is: id tag, role string, then bytes. Framebuffer attachments use role framebuffer and correlate to the JSON-RPC id of the method that returned { "encoding": "mxfb0001", "byteLength": N }.

JSON-RPC version is "2.0". Host methods that need a reply must carry an id (number or string). A hello without an id is dropped: host.v1.hello is not a notification.

CodeMeaning
-32700parse error
-32600invalid request
-32601method not found
-32602invalid params
-32000application error

params that fail to deserialize become -32602 with the serde error in message. Unknown methods become -32601 with method not found: <name>.

Handshake

host.v1.hello must be the first kind-0 frame. A later hello returns -32600: host.v1.hello is only valid as the first message on a new connection. protocolVersion must be 1. extensionId must match this socket. Read the token from MX_EXTENSION_TOKEN; it is not a CLI flag. A wrong token returns -32602: hello token is not the token issued for this extension.

The request the host accepts (this is the object the unit test round-trips):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "host.v1.hello",
  "params": {
    "token": "abc",
    "protocolVersion": 1,
    "extensionId": "com.example.one"
  }
}

The result:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": 1,
    "hostCapabilities": ["runtime.v1"],
    "sessionId": "com.example.one-a1b2c3d4e5f60708"
  }
}

sessionId is <id>- plus 8 random bytes, hex-encoded. hostCapabilities is always ["runtime.v1"] on this host.

Studio then calls extension.v1.reportStatus with no params. You have 5 seconds.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "extension.v1.reportStatus",
  "params": null
}

Reply with the status object Settings already displays. state is ready, degraded, or failed:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "state": "ready",
    "message": "watching",
    "version": "0.1.0",
    "protocolVersion": 1,
    "capabilities": ["runtime.v1"]
  }
}

On disable, reload, or host shutdown Studio calls extension.v1.shutdown (2 second timeout) and then kills the process if it is still running.

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "extension.v1.shutdown",
  "params": null
}

Reply any JSON-RPC success, then exit.

A session that speaks those messages

This process uses the env the host actually injects, frames the way frame.rs frames, and answers the two methods the host calls. After ready it calls host.v1.listVms and prints the result array the runtime page documents.

#!/usr/bin/env python3
import json
import os
import socket
import struct
import sys
 
HOST = os.environ["MX_EXTENSION_HOST"]
TOKEN = os.environ["MX_EXTENSION_TOKEN"]
EXTENSION_ID = os.environ["MX_EXTENSION_ID"]
PROTOCOL = int(os.environ["MX_EXTENSION_PROTOCOL"])
VERSION = "0.1.0"
 
 
def recvall(sock, n):
    buf = bytearray()
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise ConnectionError("socket closed")
        buf.extend(chunk)
    return bytes(buf)
 
 
def read_frame(sock):
    length = struct.unpack(">I", recvall(sock, 4))[0]
    payload = recvall(sock, length)
    return payload[0], payload[1:]
 
 
def write_json(sock, obj):
    body = json.dumps(obj, separators=(",", ":")).encode("utf-8")
    payload = bytes([0]) + body
    sock.sendall(struct.pack(">I", len(payload)) + payload)
 
 
def reply(sock, rpc_id, result):
    write_json(sock, {"jsonrpc": "2.0", "id": rpc_id, "result": result})
 
 
host, port = HOST.rsplit(":", 1)
sock = socket.create_connection((host, int(port)))
write_json(
    sock,
    {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "host.v1.hello",
        "params": {
            "token": TOKEN,
            "protocolVersion": PROTOCOL,
            "extensionId": EXTENSION_ID,
        },
    },
)
kind, body = read_frame(sock)
hello = json.loads(body)
if hello.get("error"):
    sys.stderr.write(json.dumps(hello["error"]) + "\n")
    sys.exit(1)
 
capabilities = hello["result"]["hostCapabilities"]
next_id = 2
while True:
    kind, body = read_frame(sock)
    if kind != 0:
        continue
    msg = json.loads(body)
    method = msg.get("method")
    if method == "extension.v1.reportStatus":
        reply(
            sock,
            msg["id"],
            {
                "state": "ready",
                "message": "watching",
                "version": VERSION,
                "protocolVersion": PROTOCOL,
                "capabilities": capabilities,
            },
        )
        write_json(
            sock,
            {"jsonrpc": "2.0", "id": next_id, "method": "host.v1.listVms"},
        )
        next_id += 1
    elif method == "extension.v1.shutdown":
        reply(sock, msg["id"], None)
        break
    elif "result" in msg or "error" in msg:
        sys.stdout.write(json.dumps(msg.get("result", msg.get("error"))) + "\n")

Point command at that file, or at an interpreter with the file in args. Next: Runtime.

14 documentation articles available.