Skip to content

Server Transports

gen_servers takes your API surface — the merged ApiOutput from gen_api — and generates server-side handler code for up to three transport targets. Each transport produces the same operations with the same behavior, just adapted to its protocol.

TransportProtocolOutputUse case
HTTP/AxumREST over HTTPAxum route handlers + router functionWeb APIs, external integrations
Tauri IPCInter-process communicationTauri #[command] handlers + ipc_handler()Desktop apps with Tauri
MCPModel Context ProtocolTool definitions + handle_tool_call()AI assistant integrations

You enable each transport independently through ServerGeneratorConfig variants. Most projects use one or two; having all three is for applications that serve web clients, desktop clients, and AI tools from the same codebase.

let servers_output = ontogen::gen_servers(
Some(&api_output),
&["src/api/v1".into()],
&ontogen::ServersConfig {
api_dir: "src/api/v1".into(),
state_type: "AppState".to_string(),
service_import_path: "crate::api::v1".to_string(),
types_import_path: "crate::schema".to_string(),
state_import: "crate::AppState".to_string(),
naming: ontogen::servers::NamingConfig::default(),
generators: vec![
ontogen::servers::ServerGenerator::HttpAxum {
output: "src/api/transport/http/generated.rs".into(),
},
ontogen::servers::ServerGenerator::TauriIpc {
output: "src/api/transport/ipc/generated.rs".into(),
},
ontogen::servers::ServerGenerator::Mcp {
output: "src/api/transport/mcp/generated.rs".into(),
},
],
client_generators: vec![],
rustfmt_edition: "2024".to_string(),
sse_route_overrides: Default::default(),
ts_skip_commands: vec![],
route_prefix: None,
store_type: Some("Store".to_string()),
store_import: Some("crate::store::Store".to_string()),
pagination: None,
// Required when using the AdminRegistry client generator. Pipeline
// forwards this from parse_schema automatically; explicit calls must
// pass it manually. Pass Vec::new() if not using admin-registry.
schema_entities: schema.entities.clone(),
},
)?;

Let’s walk through the important fields:

FieldPurpose
api_dirDirectory to scan for API source files
state_typeYour AppState type name, used in handler signatures
service_import_pathRust import path for API modules (e.g., crate::api::v1)
types_import_pathImport path for schema/DTO types
state_importFull import path for the state type
namingPluralization and URL naming overrides
generatorsWhich server transports to generate
client_generatorsWhich client artifacts to generate (TypeScript transports, admin registry)
store_typeStore type name for entity-scoped functions
store_importFull import path for the Store type
schema_entitiesParsed entities (used by the admin-registry generator). Pipeline auto-fills this.

The HTTP generator produces Axum route handlers with proper HTTP methods, paths, and request/response types.

Each OpKind maps to a specific HTTP method and path pattern:

OperationMethodRoute patternExample
ListGET/api/{entities}GET /api/tasks
GetByIdGET/api/{entities}/:idGET /api/tasks/:id
CreatePOST/api/{entities}POST /api/tasks
UpdatePUT/api/{entities}/:idPUT /api/tasks/:id
DeleteDELETE/api/{entities}/:idDELETE /api/tasks/:id
CustomGetGET/api/{entities}/{action}GET /api/tasks/overdue
CustomPostPOST/api/{entities}/{action}POST /api/tasks/close-by-project
JunctionListGET/api/{parents}/:id/{children}GET /api/agents/:id/roles
JunctionAddPOST/api/{parents}/:id/{children}POST /api/agents/:id/roles
JunctionRemoveDELETE/api/{parents}/:id/{children}/:child_idDELETE /api/agents/:id/roles/:child_id
EventStreamGET/api/events/{name}GET /api/events/task-updates

Entity names in URLs are pluralized and kebab-cased automatically: work_session becomes work-sessions, unit_of_work becomes units-of-work.

Here’s what a generated list handler looks like:

async fn task_list(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<Task>>, ApiError> {
let store = state.store().await.map_err(|e| err(e.to_string()))?;
task::list(store)
.await
.map(Json)
.map_err(|e| err(e.to_string()))
}

The generator also produces a router() function that wires all handlers to their routes:

pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/api/tasks", get(task_list).post(task_create))
.route("/api/tasks/:id", get(task_get_by_id).put(task_update).delete(task_delete))
// ... more routes
}

When you set pagination in your config, all List operations get limit and offset query parameters:

pagination: Some(ontogen::servers::PaginationConfig {
default_limit: 50,
max_limit: 200,
}),

List handlers then return PaginatedResult<T> instead of Vec<T>:

#[derive(Serialize)]
pub struct PaginatedResult<T: Serialize> {
pub items: Vec<T>,
pub total: u64,
pub limit: u32,
pub offset: u32,
}

The pagination wraps the service call result — it fetches all items, then applies limit/offset in memory. The total field reflects the unfiltered count so clients can calculate page counts.

The IPC generator produces #[tauri::command] handlers for desktop applications. Each command follows an entity-first naming convention:

API functionIPC command name
task::listtask_list
task::get_by_idtask_get_by_id
task::createtask_create
agent::add_roleagent_add_role
task::get_overduetask_get_overdue
#[tauri::command]
pub async fn task_list(
state: State<'_, Arc<AppState>>,
) -> Result<Vec<Task>, String> {
let store = state.store().await.map_err(|e| e.to_string())?;
task::list(store).await
.map_err(|e| e.to_string())
}

The generator also produces an ipc_handler() function that wraps all commands into a tauri::generate_handler! macro call:

pub fn ipc_handler() -> impl Fn(tauri::ipc::Invoke) -> bool + Send + Sync + 'static {
tauri::generate_handler![
task_list,
task_get_by_id,
task_create,
task_update,
task_delete,
// ... all commands
]
}

Wire this into your Tauri builder:

tauri::Builder::default()
.invoke_handler(generated::ipc_handler())
.run(tauri::generate_context!())

For modules with event stream functions, the IPC generator produces a start_event_forwarding function that bridges your event bus to Tauri’s event system:

pub fn start_event_forwarding(app_handle: tauri::AppHandle, state: &AppState) {
// For each event stream, spawns a task that receives from the broadcast
// channel and emits Tauri events to the frontend
}

The MCP (Model Context Protocol) generator produces tool definitions for AI assistants. Each API function becomes an MCP tool with a JSON Schema input definition, a description, and an async handler.

pub struct McpToolDef {
pub name: &'static str,
pub description: &'static str,
pub schema_fn: fn() -> Value,
pub handler: HandlerFn,
}

The generated registry provides three entry points:

  • generated_tool_registry() — returns Vec<McpToolDef> with live handlers for tool execution.
  • tool_definitions() — returns Vec<SimpleToolDef> with pre-computed schemas, suitable for tools/list responses.
  • handle_tool_call(state, tool_name, args) — dispatches a tool call by name. Creates a minimal tokio runtime for sync MCP servers.

MCP tool names follow the same entity-first convention as IPC commands: task_list, task_create, agent_add_role.

URL paths and command names are derived from module names using the cruet crate for Rails-style inflection. The NamingConfig lets you override the defaults when the inflector gets it wrong.

naming: ontogen::servers::NamingConfig {
plural_overrides: HashMap::from([
("evidence".to_string(), "evidence".to_string()), // uncountable
]),
singular_overrides: HashMap::from([
("work_sessions".to_string(), "session".to_string()),
]),
label_overrides: Default::default(),
plural_label_overrides: Default::default(),
},

The naming system provides several derived forms:

MethodInputOutputUsed for
url_plural"work_session""work-sessions"HTTP route paths
url_singular"work_sessions""session"IPC command prefixes
module_plural"evidence""evidence"Rust code references
label"work_session""Work Session"Admin UI labels
plural_label"evidence""Evidence"Admin UI plural labels

For custom functions, derive_action strips the module name from the function name and converts to kebab-case: get_overdue_tasks on the task module becomes the action segment overdue.

By default, event stream functions get routes under /api/events/{kebab-name}. You can override individual routes:

sse_route_overrides: HashMap::from([
("graph_updated".to_string(), "/api/events/graph".to_string()),
]),

When your app manages multiple projects, you can prepend a project scope to all entity routes:

route_prefix: Some(ontogen::servers::RoutePrefix {
segments: "projects/:project_id".to_string(),
state_accessor: "store_for".to_string(),
params: vec![
ontogen::servers::PrefixParam {
name: "project_id".to_string(),
rust_type: "uuid::Uuid".to_string(),
ts_type: "string".to_string(),
},
],
}),

This generates scoped routes like /api/projects/:project_id/tasks alongside the entity handlers. The state_accessor method is called to construct a project-scoped Store from the extracted path parameter.

Each transport writes to a single file specified in its config:

src/api/transport/
http/
generated.rs # Axum routes + router()
ipc/
generated.rs # Tauri commands + ipc_handler()
mcp/
generated.rs # MCP tools + tool_definitions() + handle_tool_call()

All three files are regenerated on every build. Don’t edit them.