π¦ Rust Plugins¶
A Rust plugin can be one .rs file. Add a command, return rows of data, or create a
connector with a few functions. Meerschaum handles building and loading it.
Save your files in the plugins directory (normally ~/.config/meerschaum/plugins;
see custom paths). You need a Rust toolchain
with cargo available for the first build and after edits. The first build may
download dependencies; later runs reuse the compiled plugin.
To create a starter file, run mrsm bootstrap plugins greetings and choose Rust
when the wizard asks which language to use. If you pick an action, say sing song,
the starter defines #[make_action] fn sing_song(...), which mrsm sing song runs.
The wizard offers the same features as for Python plugins. Keep the default Single file
for a greetings.rs like the ones below, or choose Package for a Cargo crate on the
full SDK (greetings/Cargo.toml, see the advanced guide).
Use mrsm edit plugins greetings to reopen it. Upload it to a repository with
mrsm register plugins greetings -r api:your-repo, just as you would a Python plugin.
Add a command¶
Save this as greetings.rs:
Run it:
The function name becomes the command. The /// comment becomes its help text.
To accept names from the command line, replace the function with:
#[make_action]
fn greet(action: Vec<String>) -> (bool, String) {
(true, format!("Hello, {}!", action.join(", ")))
}
mrsm greet Alice Bob returns Hello, Alice, Bob!. The returned pair contains
whether the command succeeded (true or false) and a message.
Provide data¶
Save this as readings.rs. Each json! object is one row:
fn fetch() -> Vec<Value> {
vec![json!({"id": 1, "temperature": 21.5})]
}
fn register() -> Value {
json!({"columns": {"id": "id"}})
}
fetch supplies the data. The optional register function supplies Pipe settings;
here, id identifies each row. Register a Pipe, then sync it:
mrsm register pipe -c plugin:readings -m readings -i sql:local
mrsm sync pipes -c plugin:readings -i sql:local
The filename supplies the plugin name: readings.rs becomes plugin:readings.
Value and json! are available without imports. Return the final expression
without a trailing semicolon, as shown above.
Create a connector¶
To provide your own connector type, put its functions in a marked module:
#[make_connector]
mod readings {
fn fetch(connector: &Connector) -> Vec<Value> {
vec![json!({"id": 1, "source": connector.label()})]
}
}
This creates readings:<label> connectors. For example:
mrsm register pipe -c readings:office -m readings -i sql:local
mrsm sync pipes -c readings:office -i sql:local
The label is office in this example. Use connector.config() to read its
configuration. You can add register inside the module to supply Pipe settings.
One file may contain several actions, connector modules, and helper functions.
Python sees the same types: mrsm.get_connector('readings:office') returns a connector
whose fetch(), register() and other module functions run in Rust, and
mrsm.Pipe('readings:office', 'readings', instance='sql:local') syncs from it.
Run code around syncs¶
Mark functions with #[pre_sync_hook] or #[post_sync_hook] to run them before or
after every sync, like Python's sync hooks:
#[post_sync_hook]
fn report(pipe: &Pipe, success_tuple: SuccessTuple, sync_duration: f64) -> (bool, String) {
(true, format!("{} took {sync_duration:.1}s: {}", pipe.metric_key(), success_tuple.message))
}
mrsm sync pipes prints each hook's message. A failing hook never stops the sync.
Talk to Meerschaum¶
Your functions can read and write other pipes, query the database, and keep settings.
host and Query are available without imports, like Pipe. This sync rebuilds a
pipe from another one:
fn sync(pipe: &Pipe, begin: Option<Bound>, end: Option<Bound>) -> Result<SuccessTuple> {
// Another pipe, by its keys. It is on the same instance as `pipe`.
let readings = host::pipe("plugin:readings", "readings", None, None)?;
// Your plugin's settings: the `plugins: readings:` section of `mrsm edit config`.
let factor = host::plugin_config()?["factor"].as_f64().unwrap_or(1.0);
// Its rows between `begin` and `end`, as JSON objects.
let mut rows = readings.get_data(begin.clone(), end.clone(), None)?;
for row in &mut rows {
row["temperature"] = json!(row["temperature"].as_f64().unwrap_or(0.0) * factor);
}
// Replace this pipe's rows between `begin` and `end` with the new ones, all at once.
pipe.replace(rows, begin, end)
}
Run SQL on a pipe's database with host::read_sql, which returns rows:
let table = readings.target();
let counts = host::read_sql(
pipe.instance_keys(),
&format!("SELECT station, COUNT(*) AS n FROM {table} GROUP BY station"),
)?;
| Call | What it does |
|---|---|
host::pipe("connector", "metric", location, instance) |
Any pipe, with its registered parameters. None for the location or the instance (the same instance). |
pipe.get_data(begin, end, params) |
The rows in [begin, end) matching params, like Python's pipe.get_data(). |
pipe.get_data_with(&Query::new().order("desc").limit(1)) |
The same, with select, omit, order and limit: here, the newest row. |
pipe.get_rowcount(begin, end, params) |
How many rows. |
pipe.get_sync_time() |
The newest value of the datetime column (None when empty). |
pipe.sync(rows) |
Write rows (a pipe that is not registered yet is registered first). |
pipe.replace(rows, begin, end) |
Replace the rows in [begin, end) with rows in one step, so nobody sees the window half rewritten. A None bound comes from the rows. |
pipe.clear(begin, end, params) |
Delete rows; None bounds clear everything. |
pipe.exists(), pipe.get_columns_types(), pipe.target() |
Whether the table exists, its columns, its name. |
host::read_sql("sql:main", query) |
The rows of a query on a SQL connector. |
host::exec_sql("sql:main", statement) |
Run a statement that returns no rows. |
host::plugin_config(), host::write_plugin_config(&json!({...})) |
Read or replace your plugin's settings, as Python's get_plugin_config() does. |
These work in the functions Meerschaum calls: fetch, sync, register, setup, hooks,
actions, streams, executors, routes, MCP entries and console pages. In routes, MCP entries and
pages they act for the caller, who needs the matching scope
(details). Bounds are Bound::Int(5), Bound::from("2024-01-01") or a value
you were given; bound.timestamp() turns a datetime bound into Unix seconds and
Bound::from_timestamp(seconds) back. Each call returns a Result, so end it with ?.
Writes to a protected instance are refused unless you pass --allow-protected.
Add flags, a version, and setup¶
Give your commands their own flags with #[add_plugin_argument(...)], which takes the
same arguments as Python's add_plugin_argument (except nargs; pick flags the built-in
CLI does not already use, see mrsm --help):
const VERSION: &str = "1.0.0";
#[add_plugin_argument("--loud", action = "store_true", help = "Shout.")]
#[make_action]
fn greet(kwargs: &Value) -> (bool, String) {
let text = if kwargs["loud"] == true { "HELLO!" } else { "Hello!" };
(true, text.to_string())
}
fn setup() -> (bool, String) {
(true, "Ready.".into())
}
mrsm greet --loud returns HELLO!. VERSION is the plugin's version when you
share it through a repository, and setup runs during mrsm setup plugins greetings.
List Python packages or other plugins your plugin needs in
const REQUIRED: &[&str] = &["plugin:other"];, like Python's required.
Serve HTTP routes and MCP tools¶
Mark a function with #[api_route(...)] to serve it from mrsm start api, next to the
core API, like Python's @api_plugin:
/// One item.
#[api_route(GET, "/greetings/items/{id}", scopes = ["pipes:read"])]
fn item(request: &RouteRequest) -> Value {
json!({"id": request.path_param("id"), "user": request.principal})
}
Callers need the API's credentials and every listed scope; add public = true (and no
scopes) for a route anyone may call. Return JSON, text, bytes or a RouteResponse.
A route's host calls act for its caller: reading pipes needs the
pipes:read scope, syncing pipes:write, clearing pipes:delete, and registering
pipes:register. Pipe visibility, edit rights and connector teams apply too. A public route's
anonymous callers have no scopes; to serve Everyone's pipe data, add
host_scopes = ["pipes:read"].
Mark functions with #[mcp_tool], #[mcp_resource] or #[mcp_prompt] to offer them
to AI agents through /mcp and mrsm start mcp:
/// Add two integers.
#[mcp_tool(input_schema = r#"{"type": "object", "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}}}"#)]
fn add(arguments: &Value) -> Value {
json!({"sum": arguments["a"].as_i64().unwrap_or(0) + arguments["b"].as_i64().unwrap_or(0)})
}
The /// comment is the tool's description.
Run jobs and stream data¶
Mark a module with #[make_executor] to run Meerschaum jobs your own way: the jobs of
-e procs:<label> (for example mrsm sync pipes -d -e procs:main) go to its functions,
which are the executor's methods. Leave out the ones you do not need:
#[make_executor]
mod procs {
fn get_job_names() -> Vec<String> {
vec![]
}
fn create_job(name: &str, sysargs: Vec<String>) -> (bool, String) {
(true, format!("Would run `mrsm {}` as '{name}'.", sysargs.join(" ")))
}
}
Add fn subscribe to a connector module to stream rows into mrsm sync pipes --listen
as they arrive. It returns a closure the host calls for each batch:
#[make_connector]
mod ticker {
fn subscribe() -> impl FnMut(Duration) -> Batch {
let mut next = 0;
move |_timeout| {
next += 1;
std::thread::sleep(Duration::from_secs(1));
Batch::Rows(vec![json!({"id": next})])
}
}
}
Store pipes and add console pages¶
Add instance to a connector module to make it a Meerschaum instance: -i memory:main
then registers, syncs and lists pipes through the functions named after the instance
methods (register_pipe, sync_pipe, get_pipe_data, fetch_pipes_keys, ...):
#[make_connector(instance)]
mod memory {
fn register_pipe(pipe: &Pipe) -> SuccessTuple {
SuccessTuple::ok(format!("Registered {}.", pipe.metric_key()))
}
fn sync_pipe(pipe: &Pipe, data: Vec<Value>) -> (bool, String) {
(true, format!("Received {} rows.", data.len()))
}
}
From Python, pass instance='memory:main' to mrsm.Pipe() or mrsm.get_pipes().
Mark a function with #[web_page] to add a page to the web console
(pip install meerschaum[dash]), as Python's @web_page does:
#[web_page("readings/report", login_required = false)]
fn report(page: &PageRequest) -> String {
format!("<h3>Readings</h3><p>Hello, {}.</p>", page.username.as_deref().unwrap_or("guest"))
}
Every attribute¶
| Attribute or function | Python equivalent | Example |
|---|---|---|
#[make_action] |
@make_action |
#[make_action] fn greet(action: Vec<String>) -> (bool, String) |
#[make_connector] module |
@make_connector |
#[make_connector] mod readings { fn fetch() -> Vec<Value> { ... } } |
#[make_connector(instance)] module |
@make_connector with IS_INSTANCE = True |
#[make_connector(instance)] mod memory { fn sync_pipe(pipe: &Pipe, data: Vec<Value>) -> SuccessTuple { ... } } |
fn fetch / fn sync / fn register |
fetch / sync / register |
fn fetch(pipe: &Pipe) -> Vec<Value> |
fn subscribe (top level or in a connector) |
an IS_STREAMING connector's subscribe |
fn subscribe(pipe: &Pipe) -> impl FnMut(Duration) -> Batch (or Box<dyn Subscription>) |
fn setup |
setup |
fn setup() -> bool { true } |
#[pre_sync_hook] / #[post_sync_hook] |
@pre_sync_hook / @post_sync_hook |
#[post_sync_hook] fn report(success_tuple: SuccessTuple) |
#[add_plugin_argument(...)] |
add_plugin_argument |
#[add_plugin_argument("--loud", action = "store_true")] |
const VERSION / const REQUIRED |
__version__ / required |
const VERSION: &str = "1.0.0"; |
#[api_route(...)] |
@api_plugin routes |
#[api_route(POST, "/my/items", scopes = ["pipes:write"])] |
#[mcp_tool(...)] |
MCP tools | #[mcp_tool(description = "Count the items.", read_only = true)] fn count() -> Value |
#[mcp_resource(...)] |
MCP resources | #[mcp_resource(uri_template = "my://items/{id}", description = "One item.")] fn item(resource: &ResourceRead) -> String |
#[mcp_prompt(...)] |
MCP prompts | #[mcp_prompt(arguments = ["who"])] fn greet(prompt: &PromptGet) -> String |
#[make_executor] module |
@make_executor |
#[make_executor] mod procs { fn start_job(name: &str) -> (bool, String) { ... } } |
#[web_page(...)] |
@web_page |
#[web_page("my/page", login_required = false)] fn page(page: &PageRequest) -> String |
The single-file reference lists every parameter, option and return type.
Add dependencies¶
To use another Rust crate, put a Cargo dependency block at the top of your file:
Then import it normally, for example use regex::Regex;. Versions and features
use standard Cargo syntax. Meerschaum builds the dependencies and keeps the
lockfile in its cache; no separate Cargo.toml is needed.
Learn more¶
Run mrsm show plugins to check discovery, or mrsm greet --help for an action's
help. Editing a file triggers a rebuild on the next command that loads plugins.
Only run plugins you trust.
See the single-file reference for optional arguments, hooks, flags, setup, routes, MCP entries, executors, streams, host calls, sync overrides, and dependency details. The advanced guide covers Cargo packaging, the full SDK, web extensions, testing, and distribution.