Browse topics
Calls
Typed RPC hooks. One name, one schema, one implementation - across every transport.
Calls are typed RPC hooks. Each is declared once in the central CallRegistry and callable from every transport: custom-applet iframes, MCP agents, automation scripts, and internal Rust code.
Capability required: hooks:consume (to call); the hook’s applet-type permission (e.g. docs:read, docs:write) governs what you can call.
Architecture
flowchart TB
subgraph Callers["Callers (any transport)"]
A["Custom applet iframe<br/>rp.docs.foo()"]
B["MCP agent<br/>tool: docs.foo"]
C["Automation script<br/>rp.docs.foo()"]
D["Rust internal<br/>registry.invoke()"]
end
subgraph Core["Rust CallRegistry - single source of truth"]
E[Permission check]
F{Handler type?}
end
G["Rust handler<br/>disk / DB / config"]
H["Frontend target<br/>mounted editor<br/>(TipTap, UI state)"]
A --> E
B --> E
C --> E
D --> E
E --> F
F -->|Rust| G
F -->|Frontend| H
H <-.->|"Tauri event<br/>+ one-shot reply"| I["Mounted component<br/>(claimed target)"]
Call names
Canonical format: <resource_type>.<camelCaseHook>. Examples:
docs.getOpenedFiledocs.insertAtSelection
The same name is used everywhere - TypeScript (rp.docs.getOpenedFile), MCP (docs.getOpenedFile), HTTP (POST /hooks/invoke with "name": "docs.getOpenedFile"), and Rust (registry.invoke("docs.getOpenedFile", ...)).
From your custom applet
import { createHooksProxy, type HookTransport } from "@rightplace/sdk";
const transport: HookTransport = {
invoke: async (name, params) => {
// Send to the main app via the postMessage bridge.
return ipc.request("hooks:invoke", { name, params });
},
};
const rp = createHooksProxy(transport);
const file = await rp.docs.getOpenedFile({ resource_id: resourceId });
// file.path, file.content, file.is_dirty, file.cursor
await rp.docs.insertAtSelection({ resource_id: resourceId, text: "hello" });
Manifest:
{
"apiVersion": 1,
"capabilities": ["hooks:consume", "docs:read", "docs:write"],
"hooks": {
"calls": ["docs.getOpenedFile", "docs.insertAtSelection"]
}
}
Both the capabilities (coarse) and the hooks.calls allowlist (per-hook) must list the permission/hook before the call is allowed. Users see the allowlist at install time.
Error model
| Error | When | What to do |
|---|---|---|
UnknownHook | Name not in registry | Check spelling / SDK version |
PermissionDenied | Missing capability or allowlist | Add to manifest, reinstall |
InvalidParams | Params don’t match schema | Check the hook’s schema |
TargetNotAvailable | Frontend handler, editor not mounted | Prompt user to open the file, or fail |
TargetTimeout | Claimed but didn’t reply in 5s | Retry or fail |
HandlerError | Handler threw | Pass-through error message |
Discovery
// From the main app, via Tauri:
const hooks = await invoke<Array<{ name: string; description: string }>>("cmd_hook_list");
const searchResults = await invoke("cmd_hook_search", { query: "highlight text" });
Keyword-match over name + description. Vector search is planned.
Position semantics
Hooks that take or return positions (docs.moveCursor, docs.setSelection, etc.) use UTF-16 code-unit offsets into the applet’s serialized text (e.g., markdown source). Matches JavaScript’s native string indexing.
Catalog
Current applets that expose call-hooks: