Skip to main content

Chapter 22: Interacting with the Host Database

TL;DR

  1. Define your schema: Create a sql/ directory in your pack's root. Add .sql files with CREATE TABLE statements (e.g., 001_my_table.sql). The files are executed alphabetically on install.

  2. No direct connection: Your backend server cannot connect to the database directly. All access is brokered by the Host for security.

  3. Query from your backend: To run a query, elicit a worka_action targeting the worka/db virtual pack.

  4. Use the db_query tool: Provide your query string and params 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.