Chapter 22: Interacting with the Host Database
TL;DR
-
Define your schema: Create a
sql/
directory in your pack's root. Add.sql
files withCREATE TABLE
statements (e.g.,001_my_table.sql
). The files are executed alphabetically on install. -
No direct connection: Your backend server cannot connect to the database directly. All access is brokered by the Host for security.
-
Query from your backend: To run a query,
elicit
aworka_action
targeting theworka/db
virtual pack. -
Use the
db_query
tool: Provide yourquery
string andparams
to the tool. The Host will execute the query against your pack's private schema and return the rows.Example Elicitation (conceptual):
{
"_meta": {
"worka_action": {
"target": { "tenant": "worka", "name": "db" },
"tool": "db_query",
"params": {
"query": "SELECT * FROM my_private_table WHERE id = $1",
"params": [123]
}
}
}
}
Many packs need to store their own data—user settings, created items, session information, etc. Worka provides a robust, secure, and isolated persistence model for every pack using the central PostgreSQL database managed by the Host.
Step 1: Defining Your Pack's Schema
To create your own private tables, you start by creating a sql/
directory in the root of your pack.
Inside this directory, you place standard .sql
files containing your CREATE TABLE
statements and any other DDL (Data Definition Language) commands.
There is a critical convention you must follow: the Host executes these files alphabetically during pack installation. Therefore, you must name your files with a numeric prefix to control their execution order.
Example Structure:
my-pack/
└── sql/
├── 001_create_authors_table.sql
├── 002_create_posts_table.sql
└── 003_add_indexes.sql
When your pack is installed, Worka will automatically create a private schema for your pack in the database and then execute these files in order, setting up your tables.
Step 2: The worka/db
Virtual Pack
For security, your pack's container is in an isolated network and cannot connect to the Host's database directly. All database access must be brokered through the Host. This prevents a compromised pack from accessing data outside its own schema.
The interface for this brokered access is the worka/db
virtual pack.
Step 3: Querying from Your Backend
To run a query from your MCP server (e.g., in a tool function), you use the brokered elicitation pattern you learned about previously.
Your tool function constructs an elicit
request. Instead of asking for user input, this request contains a special worka_action
in its metadata, instructing the Host to perform a database query on your pack's behalf.
Let's look at a conceptual example from a Rust MCP server.
// In your tool function...
async fn get_my_data(req: ToolRequest) -> ToolResponse {
let item_id: i64 = ...; // Get ID from req.params
// 1. Define the database query and parameters
let query = "SELECT id, title, content FROM my_posts WHERE id = $1".to_string();
let params = vec![json!(item_id)];
// 2. Construct the worka_action for the database
let db_action = json!({
"target": { "tenant": "worka", "name": "db" },
"tool": "db_query",
"params": {
"query": query,
"params": params
}
});
// 3. Elicit the action from the host
let elicit_request = CreateElicitationRequestParam {
message: "Querying database".to_string(),
// The _meta field is where the action goes
meta: Some(Meta(serde_json::Map::from_iter(vec![(
"worka_action".to_string(),
db_action,
)]))),
..
};
// 4. The rmcp library sends this to the host. The host executes the query
// and the result is returned here.
match context.elicit(elicit_request).await {
Ok(response) => {
// The database rows are in response.content
ToolResponse::success(response.content.unwrap_or_default())
}
Err(e) => ToolResponse::internal_error(e.to_string()),
}
}
This secure flow ensures that your pack can only ever run queries against its own, isolated schema, providing a powerful yet safe persistence model for all packs in the ecosystem.