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
idas the request. - Notifications have no
idand 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
- Read the three environment variables.
- Open
SHIRE_PLUGIN_PIPEfor read/write. - Send
plugin.helloand parse the line response. - Implement
Request(method, params)by writing one JSON line and reading lines until the matchingid; dispatch notifications in between. - Expose
ui.setLayout,player.getPositionand the other methods you need. - Check
error.codeand 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.