rk plugins
An rk plugin is an npm package that adds commands to rk, features to rk server and the MCP server, or both. Plugins keep optional tooling out of the core CLI: nothing is loaded unless it is installed in your project.
Plugin package contract
A plugin declares what it provides in two places.
The rk field in package.json
rk field in package.jsonHelp and command routing read only this field. They never run plugin code.
{
"name": "@roku-sdk/rk-plugin-example",
"main": "index.js",
"rk": {
"api": 1,
"commands": [{ "name": "hello", "description": "Print a greeting" }]
}
}| Field | Description |
|---|---|
api | The plugin API version the package targets. |
commands | The verbs the plugin adds, each with a name and a one-line description shown in rk --help. A server-only plugin lists none. |
The commands in the manifest must match the commands the descriptor implements. A mismatch is refused when the plugin loads.
The descriptor
The package's entry point exports a descriptor:
| Member | Description |
|---|---|
name | A non-empty plugin name. |
rkPluginApi | The API version the plugin was written for. |
commands? | The command implementations, one per verb in the manifest. |
registerServer?(host) | Called once when rk server starts. Register server extensions and MCP tools through host. |
Each command needs a string name, a string help and an exec function. A descriptor that breaks this, or a registerServer that is not a function, is refused when the plugin loads.
host carries the server registration functions of the running server: registerServerExtension, registerMcpTool and registerMcpToolPatch. Register through host and do not import tooling's server modules from the plugin. A plugin loaded as CommonJS would otherwise get its own copy of the registries, and its extensions would never reach the server.
Import the types from @roku-sdk/tooling/plugin:
import type { Command, RkPlugin, RkPluginHost, RkPluginManifest } from "@roku-sdk/tooling/plugin";
import { RK_PLUGIN_API_VERSION } from "@roku-sdk/tooling/plugin";API version
The current plugin API version is 1. One rule applies on every path (rk --help, running a verb, rk server start, rk server start --daemon and the in-process stdio MCP server started by rk mcp): the api in the rk field of package.json is checked before any of the plugin's code is imported, and the descriptor's rkPluginApi is checked again after it loads. The two must be equal. An unsupported version is refused with the same message everywhere, such as:
rk plugin "@acme/rk-plugin-example" uses plugin api 2 (supported: 1); upgrade @roku-sdk/tooling.The message tells you which side to upgrade: @roku-sdk/tooling when the plugin is newer, the plugin when it is older. rk --help skips such a plugin with a warning. The API version is checked before the shape of commands and registerServer, so a plugin written for a newer API gets this message rather than a shape error.
Naming
First-party plugins are named @roku-sdk/rk-plugin-*. Third-party plugins can use any package name; list them in plugins (see below).
Where plugins are found
rk resolves plugin packages from your project root: the nearest directory, starting at the current one and walking up, that contains a roku-config.* file, or else the nearest directory that contains a package.json. This works the same whether you run a project-local rk, a globally installed rk or npx rk.
Which packages rk looks for is decided as follows:
- With no
pluginskey inroku-config.ts,rklooks for the first-party plugins and uses the ones that are installed. - With
plugins: ["@acme/rk-plugin-example"], that list replaces the first-party list. Add the first-party packages to it if you still want them. - With
plugins: [], no plugin is loaded.
Every entry in plugins must be a package name such as pkg or @scope/pkg; a path, a node: specifier or a name with uppercase letters or spaces is refused with RK_PLUGIN_INVALID.
rk never scans node_modules. A package that is installed but not named (or not first-party) is ignored.
import { RokuConfig } from "@roku-sdk/rsg-ts/roku-config";
const config: RokuConfig = {
app: { entryPoint: "src/index.ts" },
plugins: ["@roku-sdk/rk-plugin-text-measure", "@acme/rk-plugin-example"]
};A package named in plugins but not installed fails rk server start with RK_PLUGIN_NOT_INSTALLED; the message includes the install command for your package manager. For verbs and rk --help, a package that cannot be resolved (not installed, an invalid manifest or an unsupported API) is skipped with one [rk] skipping plugin <package>: <message> line on stderr, because rk cannot tell which verbs it would have provided. A verb nobody declares then reports Command "<verb>" not found., so a mistyped command name is not blamed on an unrelated plugin. The exception is a first-party verb such as measure: if its package is listed or installed but broken, rk reports that package's error instead. A plugin entry that exposes only an import export condition is refused with RK_PLUGIN_INVALID ("must expose a require-able entry"); give the package a require or default condition or a main.
If two plugins declare the same verb, the first one wins and rk prints [rk] plugins <a> and <b> both declare verb <v>; using <a>. A plugin verb that equals a built-in command or one of its aliases is ignored with a [rk] plugin <package> declares reserved verb <verb>; ignoring it warning and is not listed in rk --help; the plugin's other verbs still work.
plugins is read once per process. Restart a running rk server after you change it. If roku-config.ts cannot be read, rk --help still works: rk prints a warning and falls back to the first-party defaults.
When plugin code runs
Plugins load lazily:
rk --helpand verb routing read only therkfield in each manifest.- A plugin's code is imported when you run one of its verbs, or when the server starts. The server start covers
rk serverand the in-process stdio MCP server (rk mcp) when no server is already running.
If a server is already running, rk server start reports that and exits without loading plugins. The --daemon form checks that the running server is healthy before it skips the plugin check. Otherwise, if a plugin fails to load, rk server start fails and names the package. With --daemon, rk checks that every listed plugin resolves and targets a supported API before it starts the background server; a failure inside the plugin's own code is only visible by running rk server start in the foreground. If the daemon process exits before it becomes healthy, rk stops waiting and reports the exit code with the same advice.
The two MCP paths differ on purpose:
rk serveris strict: a broken plugin stops the start.- The in-process stdio MCP server (
rk mcpwith no server running) is lenient: it writes oneplugin load failed: <package>: <message>line to stderr for each failed plugin and keeps serving the built-in tools and the healthy plugins. - A stdio process that relays to an already running server loads no plugins itself.
Plugin verbs are local-only. They do not use the server fast path, and --local is not accepted for them.
Missing plugins
If you run a first-party verb whose package is neither installed nor listed in plugins, rk prints the install command for your package manager and exits with "Command not found". If plugins in roku-config.ts leaves the package out, the message tells you to add it there as well.
If the package is listed in plugins but not installed, the verb fails with RK_PLUGIN_NOT_INSTALLED and exit code 1. The message includes the install command for your package manager.
Example plugin
package.json:
{
"name": "@acme/rk-plugin-hello",
"version": "1.0.0",
"main": "index.js",
"rk": { "api": 1, "commands": [{ "name": "hello", "description": "Print a greeting" }] }
}index.js:
module.exports = {
name: "hello",
rkPluginApi: 1,
registerServer(host) {
host.registerServerExtension({ name: "hello", isEnabled: () => true });
},
commands: [
{
name: "hello",
help: "Usage: rk hello [name]\n\nPrints a greeting.",
exec: async args => {
console.log(`Hello, ${args[0] ?? "world"}!`);
}
}
]
};Install it in your app, list it in roku-config.ts (plugins: ["@acme/rk-plugin-hello"]), and run the hello verb with a name argument.
Related documentation
- rk server: Starts the server that loads plugin server features.
- AI integration via MCP: Describes the MCP server plugins can extend.
- App configuration: Describes
roku-config.ts.
Updated about 3 hours ago