Chio/Docs
LOGIN · JOIN

Native Tool Server

Expose your own Rust functions to an agent, grant it some of them, and read the signed receipt for every call.

Guide chio@5f63e6e

On this page

A native service puts your application’s Rust functions in front of an agent. The service’s signed manifest says what can be called, a capability from your kernel says what this agent may call, and the kernel checks each request sent through it against that capability before your handler runs.

Starts from chio new pricing-tools --example pricing-tools: Larkfield's quote and refund as native tools on the pricing server. The host registers both, grants the agent two quotes, and saves the signed receipt for each decision. No model API key is needed.

Start
Four calls to one native serviceChoose an outcome.
aagentquote × 2Kkerneldservice pricingsigned manifestquote, refundρreceiptaagentquote × 2Kkerneldservice pricingsigned manifestquoterefundρreceipt

Two quotes pass every check

The capability names quote on server pricing and allows two invocations. The kernel checks it, dispatches to the handler, and signs a receipt for each call before the price returns.

call-1, call-2"price":"21.60"

The four calls the pricing-tools example captures, with the capability for quote on server pricing limited to two invocations. Guards, the receipt store, and manifest verification at registration are left out.

1. Run the governed pricing tools

Complete Installation, then pull the project, or download it. It carries its lockfile and Rust toolchain, and Cargo fetches the pinned Chio crates.

Terminalbash
chio new pricing-tools --example pricing-tools && cd pricing-tools
cargo run --release --locked
Output
call-1: ALLOW {"currency":"USD","percent":10.0,"price":"21.60","sku":"sku-118"}
call-2: ALLOW {"currency":"USD","percent":25.0,"price":"43.50","sku":"sku-204"}
outside-grant: DENY requested tool refund on server pricing is not in capability scope
exhausted-limit: DENY invocation budget exhausted for capability cap-01a12eff-a57c-76f4-941e-55509841be30
evidence: runs/91c940d4-9111-411e-9d85-cdefe526aa69

You should see the four calls in the figure: two quotes, a refusal for refund, and a refusal when a third quote meets the two-call limit. The run directory printed after evidence: holds a complete signed receipt for each decision. The first build compiles Chio's dependencies, and later runs reuse them.

2. Change what the tool does

src/service.rs owns the operation. The handler reads and checks its arguments, then returns the price:

src/service.rs · the quote toolrust
        .tool(
            NativeTool::new(
                "quote",
                "Quote a SKU at a discount, rounded half up to the cent",
                json!({
                    "type": "object",
                    "properties": {"sku": {"type": "string"}, "percent": {"type": "number"}},
                    "required": ["sku", "percent"]
                }),
            )
            .read_only(),
            |arguments| {
                let sku = text(&arguments, "sku")?;
                let percent = arguments
                    .get("percent")
                    .and_then(Value::as_f64)
                    .filter(|percent| (0.0..=100.0).contains(percent))
                    .ok_or_else(|| incomplete("percent must be between 0 and 100"))?;
                let cents = PRICES
                    .iter()
                    .find(|(id, _)| *id == sku)
                    .map(|(_, cents)| *cents)
                    .ok_or_else(|| incomplete("unknown sku"))?;
                // Basis points keep the arithmetic exact; the last step rounds half up.
                let basis = (percent * 100.0).round() as i64;
                let price = (cents * (10_000 - basis) + 5_000) / 10_000;
                Ok(json!({"sku": sku, "percent": percent, "price": usd(price), "currency": "USD"}))
            },
        )

The handler checks its arguments itself, because a declared JSON Schema describes the input to callers. .read_only() sets has_side_effects = false in the manifest, and the kernel keeps its own side-effecting classification for the call.

3. Change what the agent may call

src/host.rs owns permission. Its initial grant allows quote twice. Change these two constants to allow one refund instead, then rerun the same command:

src/host.rs · authorize one refundrust
pub const ALLOWED_TOOL: &str = "refund";
pub const CALL_LIMIT: u32 = 1;
Output
call-1: ALLOW {"currency":"USD","order":"order-77","refunded":"58.00"}
outside-grant: DENY requested tool quote on server pricing is not in capability scope
exhausted-limit: DENY invocation budget exhausted for capability cap-01a12eff-9387-71a9-8112-1804a5d685fc
evidence: runs/744d6779-509d-4566-a007-d0518cbc158f

You should see the refund of order-77, a refusal for quote, and a refusal for a second refund. The host issues the capability from these constants and runs its limit demonstration from them, so the grant changes without touching the handler or the manifest.

4. Verify a refusal with your kernel’s key

The host verifies the signed manifest before it registers the service, installs SQLite before issuance, and verifies each returned receipt signature before saving it. The project also writes kernel-public-key.txt and includes a single-record verifier. Run it on the scope refusal inside your run directory:

Terminal · the run directorybash
cargo run --release --locked --bin verify-receipt -- \
  outside-grant-receipt.json kernel-public-key.txt
Output
{
  "authorized": false,
  "crypto_error": null,
  "crypto_floor": "allow_classical",
  "decision": {
    "guard": "kernel",
    "reason": "requested tool refund on server pricing is not in capability scope",
    "verdict": "deny"
  },
  "ok": true,
  "parameter_hash_valid": true,
  "receipt_id": "0c8dc3e4b6ebe95b7cca1d994c77dbd62e293e9cb1f091a6ac3a5a82eb00b020",
  "receipt_id_valid": true,
  "signature_valid": true,
  "signer_trusted": true
}

You should see ok: true with authorized: false: an authentic record of a refusal, signed by your kernel. The kernel mediates the tool calls sent through evaluate_tool_call; host code that invokes a handler directly takes the route the figure draws around the kernel, with no check and no receipt.

Open manifest.json, capability.json, and the receipt files beside it to read every record in full. The example keeps keys, revocation state, and call counters in memory for one run, while SQLite and the JSON files retain its evidence. Read the complete request path before you adapt the host to a long-running application.

Read more in Audit the pricing tools, which finds the two denials in this same store, and Verify Receipts Offline.

Native authoring reference

Native service or MCP wrapping

A tool server reaches the kernel in one of two ways: wrap an existing MCP server with chio mcp serve, or implement a native service with the Rust SDK. Wrap when you already run a working MCP server and want governance without code changes. Build native when you want an in-process call path, a price on each tool, or one self-contained Rust binary.

ConsiderationMCP wrappingNative server
Existing server
Proxied with zero modifications
Rewritten as handlers
Typed schemas
Inherited from the MCP server
Declared per tool with JSON Schema
Pricing metadata
Added at the proxy layer
Declared inline on each tool
Call path
Extra process and JSON-RPC hop
In-process function call
Resources and prompts
Proxied from the MCP server
Registered on the builder
Manifest signing
The proxy generates and signs
The server signs its own

The Cargo project

The download is self-contained: Cargo fetches immutable revisions of the public Chio crates, and an empty workspace declaration keeps the project independent of any enclosing workspace. The manifest below is extracted from the same project. Authoring APIs also work without a kernel, but direct service calls produce no governed receipts.

Cargo.toml · pricing-toolstoml
[package]
name = "pricing-tools"
version = "0.1.0"
edition = "2021"
default-run = "pricing-tools"

[workspace]

[dependencies]
chio-core = { package = "chio-core-types", git = "https://github.com/backbay-labs/chio", rev = "1346840e1192b5d6baf581c447cd37550e5c0144" }
chio-kernel = { git = "https://github.com/backbay-labs/chio", rev = "1346840e1192b5d6baf581c447cd37550e5c0144" }
chio-manifest = { git = "https://github.com/backbay-labs/chio", rev = "1346840e1192b5d6baf581c447cd37550e5c0144" }
chio-mcp-adapter = { git = "https://github.com/backbay-labs/chio", rev = "1346840e1192b5d6baf581c447cd37550e5c0144" }
chio-store-sqlite = { git = "https://github.com/backbay-labs/chio", rev = "1346840e1192b5d6baf581c447cd37550e5c0144" }

The native module of chio-mcp-adapter provides NativeChioServiceBuilder, NativeTool, NativeResource, and NativePrompt, the authoring helpers that implement the kernel traits for you. chio-core provides cryptographic types, including chio_core::crypto::Keypair.

NativeTool

NativeTool::new takes a name, a description, and a JSON Schema for the input:

Tool definitionrust
use chio_mcp_adapter::native::NativeTool;

let quote_tool = NativeTool::new(
    "quote",
    "Quote a SKU at a discount, rounded half up to the cent",
    serde_json::json!({
        "type": "object",
        "properties": {
            "sku": { "type": "string", "description": "A Larkfield SKU, such as sku-118" },
            "percent": { "type": "number", "minimum": 0, "maximum": 100 }
        },
        "required": ["sku", "percent"]
    }),
);
  • .output_schema(json) declares the JSON Schema of the return value.
  • .read_only() marks the tool as having no side effects in its manifest (has_side_effects = false).
  • .latency_hint(LatencyHint::Instant) hints at the expected response time.
  • per_invocation_price(), flat_price(), per_unit_price(), and hybrid_price() set the price (see Tool pricing).
Tool definition · all metadatarust
let quote_tool = NativeTool::new(
    "quote",
    "Quote a SKU at a discount, rounded half up to the cent",
    serde_json::json!({
        "type": "object",
        "properties": {
            "sku": { "type": "string" },
            "percent": { "type": "number", "minimum": 0, "maximum": 100 }
        },
        "required": ["sku", "percent"]
    }),
)
.output_schema(serde_json::json!({
    "type": "object",
    "properties": {
        "sku": { "type": "string" },
        "price": { "type": "string" },
        "currency": { "type": "string" }
    }
}))
.read_only()
.per_invocation_price(1, "USD")
.latency_hint(chio_manifest::LatencyHint::Instant);

NativeChioServiceBuilder

The builder wires tools, resources, and prompts into a NativeChioService that implements the kernel’s ToolServerConnection, ResourceProvider, and PromptProvider traits. The constructor takes a server ID and a hex-encoded Ed25519 public key:

Service builderrust
use chio_mcp_adapter::native::NativeChioServiceBuilder;

NativeChioServiceBuilder::new("pricing", public_key_hex)
    .server_name("Larkfield pricing")
    .server_version("1.0.0")
    .server_description("Quotes and refunds on Larkfield's price table")

Tool handlers

Chain .tool(definition, handler) for each tool. The handler receives the arguments as a serde_json::Value and returns Result<Value, KernelError>. The Err arm is where a tool refuses, so a field the input schema declares required is read as required here too:

Tool handlerrust
.tool(
    quote_tool,
    |arguments| {
        let sku = arguments
            .get("sku")
            .and_then(|value| value.as_str())
            .ok_or_else(|| {
                KernelError::RequestIncomplete("quote requires string field sku".to_string())
            })?;
        let percent = arguments
            .get("percent")
            .and_then(|value| value.as_f64())
            .filter(|percent| (0.0..=100.0).contains(percent))
            .ok_or_else(|| {
                KernelError::RequestIncomplete("percent must be between 0 and 100".to_string())
            })?;
        Ok(serde_json::json!({ "sku": sku, "percent": percent, "price": price(sku, percent)? }))
    },
)

.tool_with_nested_flow(definition, handler) also hands the handler an optional NestedFlowBridge, for a tool that drives further governed work of its own.

Resources

Resources expose static or dynamic data that agents read. NativeResource defines the URI, name, description, and MIME type, and .static_resource() registers fixed content:

Static resourcerust
use chio_core::ResourceContent;
use chio_mcp_adapter::native::NativeResource;

.static_resource(
    NativeResource::new("memory://pricing/price-list", "Larkfield price list")
        .description("List prices in USD, by SKU")
        .mime_type("text/csv"),
    vec![ResourceContent {
        uri: "memory://pricing/price-list".to_string(),
        mime_type: Some("text/csv".to_string()),
        text: Some("sku,list\nsku-118,24.00\nsku-204,58.00\nsku-301,140.00\n".to_string()),
        blob: None,
        annotations: None,
    }],
)

For dynamic content, .resource(definition, handler) takes a handler that receives the URI and returns Result<Option<Vec<ResourceContent>>, KernelError>.

Prompts

Prompts are pre-built message sequences. NativePrompt with .static_prompt() registers a fixed one:

Static promptrust
use chio_core::{PromptMessage, PromptResult};
use chio_mcp_adapter::native::NativePrompt;

.static_prompt(
    NativePrompt::new("draft_quote_email")
        .description("Asks for a short email that presents a quote to a customer"),
    PromptResult {
        description: Some("Quote email prompt".to_string()),
        messages: vec![PromptMessage {
            role: "user".to_string(),
            content: serde_json::json!({
                "type": "text",
                "text": "Write a two-sentence email presenting this quote to Priya Raman at Harbor Goods."
            }),
        }],
    },
)

Resource and prompt registrations define provider behavior, separate from a tool capability. A host that mediates them wires their provider and authorization paths as well; the sample project registers tools.

build()

.build() validates the tool manifest and returns Result<NativeChioService, ManifestError>, so validation runs during service construction, before you register or invoke the service.

Buildrust
let service = builder.build().expect("valid manifest");

The complete pricing service

The project's src/service.rs puts the pieces together: two tools with their schemas, one read-only, each handler checking its own arguments, and .build() at the end. An invalid manifest is a ManifestError the caller handles before the first invocation.

src/service.rs · completerust
use chio_kernel::KernelError;
use chio_mcp_adapter::native::{NativeChioService, NativeChioServiceBuilder, NativeTool};
use serde_json::{json, Value};

/// Larkfield list prices in cents, as main holds them after the Quickstart.
const PRICES: [(&str, i64); 3] = [("sku-118", 2400), ("sku-204", 5800), ("sku-301", 14000)];
/// Larkfield's one open order: its SKU and the amount paid, in cents.
const ORDERS: [(&str, &str, i64); 1] = [("order-77", "sku-204", 5800)];

pub fn build(public_key: String) -> anyhow::Result<NativeChioService> {
    Ok(NativeChioServiceBuilder::new("pricing", public_key)
        .server_name("Larkfield pricing")
        .server_version("1.0.0")
        .tool(
            NativeTool::new(
                "quote",
                "Quote a SKU at a discount, rounded half up to the cent",
                json!({
                    "type": "object",
                    "properties": {"sku": {"type": "string"}, "percent": {"type": "number"}},
                    "required": ["sku", "percent"]
                }),
            )
            .read_only(),
            |arguments| {
                let sku = text(&arguments, "sku")?;
                let percent = arguments
                    .get("percent")
                    .and_then(Value::as_f64)
                    .filter(|percent| (0.0..=100.0).contains(percent))
                    .ok_or_else(|| incomplete("percent must be between 0 and 100"))?;
                let cents = PRICES
                    .iter()
                    .find(|(id, _)| *id == sku)
                    .map(|(_, cents)| *cents)
                    .ok_or_else(|| incomplete("unknown sku"))?;
                // Basis points keep the arithmetic exact; the last step rounds half up.
                let basis = (percent * 100.0).round() as i64;
                let price = (cents * (10_000 - basis) + 5_000) / 10_000;
                Ok(json!({"sku": sku, "percent": percent, "price": usd(price), "currency": "USD"}))
            },
        )
        .tool(
            NativeTool::new(
                "refund",
                "Refund an order in full",
                json!({
                    "type": "object",
                    "properties": {"order": {"type": "string"}},
                    "required": ["order"]
                }),
            ),
            |arguments| {
                let order = text(&arguments, "order")?;
                let (id, _, paid) = ORDERS
                    .iter()
                    .find(|(id, _, _)| *id == order)
                    .ok_or_else(|| incomplete("unknown order"))?;
                Ok(json!({"order": id, "refunded": usd(*paid), "currency": "USD"}))
            },
        )
        .build()?)
}

fn text<'a>(arguments: &'a Value, field: &str) -> Result<&'a str, KernelError> {
    arguments
        .get(field)
        .and_then(Value::as_str)
        .ok_or_else(|| incomplete(&format!("{field} must be a string")))
}

fn incomplete(message: &str) -> KernelError {
    KernelError::RequestIncomplete(message.into())
}

fn usd(cents: i64) -> String {
    format!("{}.{:02}", cents / 100, cents % 100)
}

A missing argument fails closed

A quote without percent returns KernelError::RequestIncomplete from the handler, and the kernel signs the outcome. A handler that filled in a default for a field the schema declares required would move a pricing decision out of the manifest and into code nobody reviews; keep required fields required.

Authoring failures

  • .build() returns a ManifestError. Validation runs inside build, and the error names what is wrong with the declaration. Fix the declaration, then build again.
  • Signing fails with the right keypair. sign_manifest validates the manifest again and checks that the key embedded in it matches the signer. Pass NativeChioServiceBuilder::new the public key the signing keypair produces.
  • An invocation returns a handler error. Handlers return Result<Value, KernelError>, and the variant names the refusal: RequestIncomplete means the arguments did not satisfy the schema the manifest advertises.
  • A resource read returns Ok(None). No registered resource owns that exact URI. The service returns Ok(None) so a caller can try another provider.
  • A provider-native tool is refused although the server offers it. server_tools admits a tool only when the manifest lists its stable name. These trust-boundary tools default to deny, and the builder leaves the list empty.

Tool pricing

The pricing helpers write ToolPricing into the manifest, where callers read the model and currency beside the schema. The capability’s monetary limits decide what a call may spend. A tool reports its realized cost through invoke_with_cost; the native connection uses the default implementation, which reports no measured cost, and the kernel charges the authorized per-invocation ceiling when one is configured. Implement cost reporting and grant a budget for a metered tool; the sample project grants none. See Budgets & Metering.

Amounts use the currency’s minor unit: for USD, 100 units equals one dollar.

Flat price

One fixed price regardless of usage. base_price is set, with no unit price or billing unit.

Flat pricerust
NativeTool::new("lookup", "Look up a record", schema)
    .flat_price(500, "USD")

Per-invocation price

A price per call. The billing unit is set to "invocation".

Per-invocation pricerust
NativeTool::new("quote", "Quote a SKU at a discount", schema)
    .per_invocation_price(1, "USD")   // USD 0.01 per quote

Per-unit price

A price per custom billing unit: tokens processed, documents returned, or bytes transferred.

Per-unit pricerust
NativeTool::new("tokenize", "Tokenize text", schema)
    .per_unit_price(2, "USD", "1k_tokens")

Hybrid price

A base price plus a per-unit price, for a tool with fixed overhead and variable cost.

Hybrid pricerust
NativeTool::new("search", "Search documents", schema)
    .hybrid_price(25, 10, "USD", "document")
    // USD minor units: 25 cents base + 10 cents per document

Manifest signing

The tool manifest declares everything the server offers: its ID, name, version, tools with their schemas and prices, and the server’s public key. Signing it with the server’s private key authenticates those manifest bytes to that key. A receiver decides whether to trust the signer, and the handler’s behavior lies outside what the signature covers.

Sign the manifestrust
use chio_core::crypto::Keypair;

let server_kp = Keypair::generate();
let service = crate::service::build(server_kp.public_key().to_hex())?;

// sign_manifest returns a SignedManifest bundling the manifest, its Ed25519
// signature, and the signer's public key. There is no separate hash field.
let signed = chio_manifest::sign_manifest(service.manifest(), &server_kp)?;

// signed.manifest is the ToolManifest; signed.signature is the Ed25519
// signature; signed.signer_key is the public key it was signed with.
println!("signed by: {}", signed.signer_key.to_hex());

A signed manifest can be published to a registry, exchanged during capability negotiation, or verified offline. Any change to a price, a tool, or a schema invalidates the signature. Store the server keypair like any signing key: whoever holds it can sign a manifest with altered prices or tool definitions.

Manifest structure

A successful .build() leaves the NativeChioService holding a validated ToolManifest:

crates/platform/chio-manifest/src/lib.rs30-63rust
pub struct ToolManifest {
    /// Schema version. Must equal [`TOOL_MANIFEST_SCHEMA`].
    pub schema: String,

    /// The server's unique identifier.
    pub server_id: chio_core::ServerId,

    /// Human-readable server name.
    pub name: String,

    /// Server description.
    pub description: Option<String>,

    /// Semantic version of this tool server.
    pub version: String,

    /// The tools this server provides.
    pub tools: Vec<ToolDefinition>,

    /// Provider-native server tools this manifest explicitly allows.
    ///
    /// Anthropic server tools are larger trust-boundary surfaces than regular
    /// client-hosted tools. They default to deny unless the manifest lists the
    /// stable logical tool name here.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub server_tools: Vec<ServerTool>,

    /// Permissions this server requires from the host environment
    /// (filesystem paths, network access, environment variables, etc.).
    pub required_permissions: Option<RequiredPermissions>,

    /// Hex-encoded Ed25519 public key of this tool server.
    pub public_key: String,
}

The builder sets schema to chio.manifest.v1, copies each registered tool’s definition into tools, leaves server_tools empty, and leaves required_permissions unset. Each ToolDefinition carries name, description, input_schema, and optional output_schema, pricing, has_side_effects, and latency_hint. ToolPricing holds a pricing_model and optional base_price, unit_price, and billing_unit, which is why per_invocation_price(1, "USD") appears as a unit price of 1 USD minor unit per invocation with no base price.

public_key is bare lowercase hex, the form Keypair::public_key().to_hex() produces and the string the builder received. Signing checks the two against each other, so a manifest advertises the key that signed it.

Next: find the two refusals in your receipt store