Chio/Docs

BuildConnect

Native Tool Server

Build a Rust tool server for the Chio kernel protocol with typed schemas, inline pricing, and a signed manifest.

When to Use Native vs. MCP Wrapping

chio supports two modes for tool servers: wrapping an existing MCP server with chio mcp serve, or implementing a native tool server using the Rust SDK.

ConsiderationMCP WrappingNative Server
Existing serverUse the proxy with zero modificationsRewrite required
Typed schemasInherited from MCP serverDeclared per-tool with JSON Schema
Pricing metadataAdded at the proxy layerDeclared inline on each tool definition
PerformanceExtra process boundary + JSON-RPC hopIn-process function call
Resources and promptsProxied from MCP serverRegistered directly on the builder
Manifest signingProxy generates and signsServer signs its own manifest

Choose MCP wrapping when you already have a working MCP server and want governance without code changes. Choose native when you need an in-process call path, pricing declared on each tool, or a self-contained Rust binary.


Project Setup

Create a new Rust project and add the required Chio crates:

bash
$ cargo new my-tool-server
$ cd my-tool-server
Cargo.toml
[package]
name = "my-tool-server"
version = "0.1.0"
edition = "2021"

[dependencies]
chio-core = { path = "../chio/crates/core/chio-core" }
chio-kernel = { path = "../chio/crates/kernel/chio-kernel" }
chio-manifest = { path = "../chio/crates/platform/chio-manifest" }
chio-mcp-adapter = { path = "../chio/crates/protocol/chio-mcp-adapter" }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Crate overview

chio-mcp-adapter's native module provides NativeChioServiceBuilder, NativeTool, NativeResource, and NativePrompt: the high-level authoring helpers that save you from implementing kernel traits by hand. chio-core provides cryptographic primitives including chio_core::crypto::Keypair.

Defining Tools with NativeTool

Each tool is created with NativeTool::new, which takes a name, description, and a JSON Schema for the input:

rust
use chio_mcp_adapter::native::NativeTool;

let greet_tool = NativeTool::new(
    "greet",
    "Returns a personalized greeting",
    serde_json::json!({
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "The name to greet"
            }
        },
        "required": ["name"]
    }),
);

The builder pattern exposes additional metadata:

  • .output_schema(json): declare the JSON Schema for the tool's return value
  • .read_only(): mark the tool as having no side effects (sets has_side_effects = false)
  • .latency_hint(LatencyHint::Instant): hint at expected response time
  • Pricing helpers (per_invocation_price(), flat_price(), per_unit_price(), hybrid_price(), see Tool Pricing below)
rust
let greet_tool = NativeTool::new(
    "greet",
    "Returns a personalized greeting",
    serde_json::json!({
        "type": "object",
        "properties": {
            "name": { "type": "string", "description": "The name to greet" }
        },
        "required": ["name"]
    }),
)
.output_schema(serde_json::json!({
    "type": "object",
    "properties": {
        "greeting": { "type": "string" }
    }
}))
.read_only()
.per_invocation_price(25, "USD")
.latency_hint(chio_manifest::LatencyHint::Instant);

Building the Service

NativeChioServiceBuilder wires tools, resources, and prompts into a NativeChioService that implements the kernel's ToolServerConnection, ResourceProvider, and PromptProvider traits.

rendering…
Native tool server lifecycle: build, sign, serve, and dispatch in-process.

The constructor takes a server ID and a hex-encoded Ed25519 public key:

rust
use chio_mcp_adapter::native::NativeChioServiceBuilder;

NativeChioServiceBuilder::new("srv-hello", public_key_hex)
    .server_name("Hello Tool Server")
    .server_version("0.1.0")
    .server_description("A native Chio service that exposes a greeting tool")

Registering Tool Handlers

Chain .tool(definition, handler) to register each tool. The handler is a closure that receives the arguments as a serde_json::Value and returns Result<Value, KernelError>:

rust
.tool(
    greet_tool,
    |arguments| {
        let name = arguments
            .get("name")
            .and_then(|value| value.as_str())
            .unwrap_or("stranger");
        Ok(serde_json::json!({
            "greeting": format!("Hello, {name}!")
        }))
    },
)

Adding Resources

Resources expose static or dynamic data that agents can read. Use NativeResource to define URI, name, description, and MIME type. For static content, use .static_resource():

rust
use chio_core::ResourceContent;
use chio_mcp_adapter::native::NativeResource;

.static_resource(
    NativeResource::new("memory://hello/template", "Greeting Template")
        .description("A static greeting template")
        .mime_type("text/plain"),
    vec![ResourceContent {
        uri: "memory://hello/template".to_string(),
        mime_type: Some("text/plain".to_string()),
        text: Some("Hello, {name}!".to_string()),
        blob: None,
        annotations: None,
    }],
)

For dynamic resources, use .resource(definition, handler) where the handler receives the URI and returns Result<Option<Vec<ResourceContent>>, KernelError>.

Adding Prompts

Prompts are pre-built message sequences. Use NativePrompt and .static_prompt() for fixed prompts:

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

.static_prompt(
    NativePrompt::new("compose_greeting")
        .description("Creates a user prompt that asks for a polite greeting"),
    PromptResult {
        description: Some("Greeting composition prompt".to_string()),
        messages: vec![PromptMessage {
            role: "user".to_string(),
            content: serde_json::json!({
                "type": "text",
                "text": "Compose a short, polite greeting for Ada."
            }),
        }],
    },
)

Calling build()

.build() validates the tool manifest and returns the finished NativeChioService. It returns Result<NativeChioService, ManifestError>: validation failures are caught at build time, not at runtime.

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

Complete Example

This working example is based on the examples/hello-tool/ reference in the chio repository. It defines a tool, a resource, and a prompt, then invokes the tool and signs the manifest:

src/main.rs
use chio_core::crypto::Keypair;
use chio_core::{PromptMessage, ResourceContent};
use chio_kernel::{PromptProvider, ResourceProvider, ToolServerConnection};
use chio_mcp_adapter::native::{
    NativeChioServiceBuilder, NativePrompt, NativeResource, NativeTool,
};

fn build_service(public_key_hex: String) -> chio_mcp_adapter::native::NativeChioService {
    NativeChioServiceBuilder::new("srv-hello", public_key_hex)
        .server_name("Hello Tool Server")
        .server_version("0.1.0")
        .server_description(
            "A tiny native Chio service that exposes a tool, resource,              prompt, and priced manifest",
        )
        .tool(
            NativeTool::new(
                "greet",
                "Returns a personalized greeting",
                serde_json::json!({
                    "type": "object",
                    "properties": {
                        "name": {
                            "type": "string",
                            "description": "The name to greet"
                        }
                    },
                    "required": ["name"]
                }),
            )
            .output_schema(serde_json::json!({
                "type": "object",
                "properties": {
                    "greeting": { "type": "string" }
                }
            }))
            .read_only()
            .per_invocation_price(25, "USD")
            .latency_hint(chio_manifest::LatencyHint::Instant),
            |arguments| {
                let name = arguments
                    .get("name")
                    .and_then(|value| value.as_str())
                    .unwrap_or("stranger");
                Ok(serde_json::json!({
                    "greeting": format!(
                        "Hello, {name}! This greeting was served                          by a native Chio service."
                    )
                }))
            },
        )
        .static_resource(
            NativeResource::new("memory://hello/template", "Greeting Template")
                .description("A static greeting template")
                .mime_type("text/plain"),
            vec![ResourceContent {
                uri: "memory://hello/template".to_string(),
                mime_type: Some("text/plain".to_string()),
                text: Some(
                    "Hello, {name}! This greeting was served                      by a native Chio service."
                        .to_string(),
                ),
                blob: None,
                annotations: None,
            }],
        )
        .static_prompt(
            NativePrompt::new("compose_greeting")
                .description("Creates a prompt that asks for a polite greeting"),
            chio_core::PromptResult {
                description: Some("Greeting composition prompt".to_string()),
                messages: vec![PromptMessage {
                    role: "user".to_string(),
                    content: serde_json::json!({
                        "type": "text",
                        "text": "Compose a short, polite greeting for Ada."
                    }),
                }],
            },
        )
        .build()
        .expect("build native Chio service")
}

#[tokio::main]
async fn main() {
    let server_kp = Keypair::generate();
    let service = build_service(server_kp.public_key().to_hex());

    // Print the manifest
    println!("Server: {} ({})", service.manifest().name, service.server_id());
    for tool in &service.manifest().tools {
        println!("  Tool: {} - {}", tool.name, tool.description);
    }

    // Sign the manifest
    match chio_manifest::sign_manifest(service.manifest(), &server_kp) {
        Ok(_signed) => println!("Manifest signed successfully."),
        Err(error) => eprintln!("Failed to sign manifest: {error}"),
    }

    // Invoke the tool. ToolServerConnection::invoke is async.
    let result = service
        .invoke("greet", serde_json::json!({ "name": "World" }), None)
        .await
        .expect("invoke greet");
    println!("Output: {result}");
}

Tool Pricing

Every native tool can declare pricing metadata in its manifest. The kernel uses this information for economic checks: if a capability token's budget cannot cover the declared price, the call is denied before execution.

NativeTool exposes four pricing helpers:

Flat Price

A single fixed price regardless of usage. The base_price is set; there is no unit price or billing unit.

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

Per-Invocation Price

Charged once per call. The billing unit is automatically set to "invocation".

rust
NativeTool::new("greet", "Return a greeting", schema)
    .per_invocation_price(25, "USD")

Per-Unit Price

Scaled by a custom billing unit: tokens processed, documents returned, bytes transferred, etc.

rust
NativeTool::new("tokenize", "Tokenize text", schema)
    .per_unit_price(2, "USD", "1k_tokens")

Hybrid Price

Combines a base price with a per-unit price. Useful when a tool has fixed overhead plus variable cost.

rust
NativeTool::new("search", "Search documents", schema)
    .hybrid_price(25, 10, "USD", "document")
    // base_price: 25 USD + unit_price: 10 USD per document

Pricing is declarative

Pricing metadata is embedded in the tool manifest. The kernel reads it during economic checks, but the actual billing settlement is handled by the receipt and settlement layer. Declaring a price does not automatically debit an account; it enables the kernel to enforce budget constraints.

Manifest Signing

The tool manifest is a structured declaration of everything the server offers: its ID, name, version, tools (with schemas and pricing), and the server's public key. Signing the manifest with the server's private key proves that the declared tools and pricing are authentic.

rust
use chio_core::crypto::Keypair;

let server_kp = Keypair::generate();
let service = build_service(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)
    .expect("sign manifest");

// 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());

The signed manifest can be published to a registry, exchanged during capability negotiation, or verified offline. Any tampering (changing a price, adding a tool, modifying a schema) invalidates the signature.

Protect your server keypair

The server keypair is the root of trust for the manifest. If the private key is compromised, an attacker can produce a valid-looking manifest with altered pricing or tool definitions. Store it with the same care as any signing key.

Manifest Structure

When .build() succeeds, the resulting NativeChioService holds a validated ToolManifest with the following fields:

rust
ToolManifest {
    schema: "chio.manifest.v1",
    server_id: "srv-hello",
    name: "Hello Tool Server",
    description: Some("..."),
    version: "0.1.0",
    tools: vec![ToolDefinition {
        name: "greet",
        description: "Returns a personalized greeting",
        input_schema: json!({ "type": "object", ... }),
        output_schema: Some(json!({ ... })),
        pricing: Some(ToolPricing {
            pricing_model: PricingModel::PerInvocation,
            base_price: None,
            unit_price: Some(MonetaryAmount { units: 25, currency: "USD" }),
            billing_unit: Some("invocation"),
        }),
        has_side_effects: false,
        latency_hint: Some(LatencyHint::Instant),
    }],
    // Provider-native server tools (Anthropic computer_use, bash, text_editor)
    // this manifest explicitly allowlists. Empty by default: those larger
    // trust-boundary tools are denied unless their stable name is listed here.
    server_tools: vec![],
    required_permissions: None,
    public_key: "7b0f6f63...",
}

Next Steps

  • Custom Guards · implement your own guard to enforce domain-specific policies
  • Economics · understand how pricing flows into budgets, metering, and settlement
  • Receipts · every tool invocation produces a signed receipt, including cost data