shire Docs
Docs/Plugin API/Protocol

Protocol

The Host and plugins exchange newline-delimited JSON-RPC 2.0 over a Windows named pipe. This is the complete language-independent contract; the SDKs are convenience layers over it.

Pipe and launch variables

When the Host launches a plugin, it sets:

Variable Meaning
SHIRE_PLUGIN_PIPE Full pipe path, e.g. \\.\pipe\shire-…
SHIRE_PLUGIN_TOKEN Per-launch opaque token for this plugin connection
SHIRE_PLUGIN_ID The manifest id

A plugin opens the pipe with read/write access, writes the handshake, and keeps the connection open.

Framing

  • One JSON object per line, terminated by \n.
  • UTF-8. A single line must not exceed 1,048,576 bytes.
  • Empty lines are ignored.
  • Responses carry the same id as the request.
  • Notifications have no id and must not be answered.

Example request:

{"jsonrpc":"2.0","id":1,"method":"player.getPosition","params":{}}

Example success:

{"jsonrpc":"2.0","id":1,"result":{"x":123.5,"y":200.0,"z":-45.2,"scene":3}}

Example error:

{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"permission denied: player.read"}}

Handshake

The first request must be plugin.hello:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "plugin.hello",
  "params": {
    "plugin_id": "com.example.myplugin",
    "token": "…",
    "api_version": "1.0",
    "sdk": "python"
  }
}

The result contains:

{
  "api_version": "1.0",
  "host": "Shire",
  "plugin_id": "com.example.myplugin",
  "capabilities": ["api.info", "player.getPosition", "…"],
  "permissions": ["player.read", "ui.create"],
  "granted": ["player.read", "ui.create"],
  "events": ["scene.changed", "player.position", "…"],
  "ui_types": ["text", "button", "toggle", "…"]
}

A token is bound to one plugin id and one launch. An invalid token is rejected and the connection is closed. The Host permits a second connection with the same token for synchronous clients that use one pipe for requests and one for events; both are bound to the same plugin record.

Until the handshake succeeds, every other method returns permission_denied or handshake required.

Capability discovery

Call api.info after the handshake. Its capabilities, permissions, events and ui_types arrays are the authoritative list for the running Host. The same data is in sdk/contract/api.json; the contract test compares them.

Never assume a method exists because an older SDK mentions it. Check capabilities and degrade gracefully.

Permissions

The manifest declares the maximum set the plugin may request. The user approves a subset in the Plugin Manager. The Host checks the granted set on every call. granted in the handshake is the current subset; api.info returns the Host-wide permission vocabulary.

A plugin can be running while a permission is missing; calls that require it fail with -32000. The manager can approve or revoke a permission while the plugin is running.

Rate limits

  • 300 calls per second per plugin, counted after the handshake.
  • Excess calls return -32001 rate_limited.
  • The Host queues at most 2,048 inbound lines and drops/closes a connection that floods it.
  • Outbound events have a 256-line queue per connection; old events are dropped first.
  • UI layouts are capped at 64 components and 16 KiB of text.
  • Plugin storage is capped at 256 KiB.

Events

The Host sends notifications:

{
  "jsonrpc": "2.0",
  "method": "shire.event",
  "params": { "event": "player.position", "data": {"x":1, "y":2, "z":3, "scene":3} }
}

UI events use a separate notification:

{
  "jsonrpc": "2.0",
  "method": "shire.uiEvent",
  "params": { "component": "my_toggle", "type": "toggle", "value": true }
}

Subscribe with events.subscribe and cancel with events.unsubscribe. The Host samples public state at approximately 4 Hz; it does not call the Core for every plugin or every frame.

Synchronous handles and two connections

A synchronous Windows pipe handle can serialize a blocking read with a write from another thread. A minimal client that needs both requests and asynchronous events should use either:

  • an event loop/overlapped I/O (Node/.NET async), or
  • two pipe connections: one used sequentially for request/response, one read only by an event thread (C++/Rust SDKs).

The Python SDK uses a single connection and polls with PeekNamedPipe, so it never has a blocking read pending while writing.

Implementing in another language

  1. Read the three environment variables.
  2. Open SHIRE_PLUGIN_PIPE for read/write.
  3. Send plugin.hello and parse the line response.
  4. Implement Request(method, params) by writing one JSON line and reading lines until the matching id; dispatch notifications in between.
  5. Expose ui.setLayout, player.getPosition and the other methods you need.
  6. Check error.code and surface it as your language's exception/error type.

No serialization library or Shire header is required. Any language with a pipe client and a JSON parser can participate.

Shire Plugin API 1.0 · Written in English. Documentation home · API status · Changelog

Independent project. Not affiliated with HoYoverse. Never put secrets in a plugin, manifest or package.