Chio/Docs

BuildFoundations

Hello Tool

Build a native Rust Chio service with a tool, resource, prompt, signed manifest, and kernel invocation.

What it shows

  • A native service built with NativeChioServiceBuilder from chio-mcp-adapter's native module.
  • One tool (greet), one resource (memory://hello/template), one prompt (compose_greeting).
  • Manifest signing with a generated keypair via chio_manifest::sign_manifest.
  • Advertised manifest pricing (25 USD per invocation) for pre-call budget planning.
  • Late event emission via emit_event / drain_events.

Native service and wrapped MCP

The repo already has strong wrapped-MCP support through chio mcp serve. This example uses the same policy and trust model with a native service value in place of the subprocess. See Native Tool Server for migration guidance.

Use this when

You are authoring a new Chio tool from scratch in Rust and want it compiled into the same binary as the kernel call site. Don't use this if you already have a third-party MCP subprocess to govern, or if you want a remote MCP edge over stdio JSON-RPC: see Hello MCP for the protocol-edge shape, and Wrap an MCP Server for the wrapped-adapter starting point.

Prerequisites

A working Rust toolchain matching the workspace's rust-version, and a clone of the chio repo. The example depends on four Chio crates from the workspace, plus serde_json and tokio (the whole flow is async):

examples/hello-tool/Cargo.toml
[dependencies]
chio-core = { package = "chio-core-types", path = "../../crates/core/chio-core-types" }
chio-kernel = { workspace = true }
chio-manifest = { workspace = true }
chio-mcp-adapter = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }

Run it

bash
cd examples/hello-tool
cargo run

The binary prints the manifest, signs it, invokes greet, reads the resource, gets the prompt, and drains a late event. Output looks like:

text
=== Chio hello-tool example ===

Native manifest:
  Server: Hello Tool Server (srv-hello)
  Tool: greet - Returns a personalized greeting
    Pricing: PerInvocation (25 USD per invocation)

Manifest signed successfully.

Tool invocation:
  Input:  {"name":"World"}
  Output: {"greeting":"Hello, World! This greeting was served by a native Chio service."}

Resource read:
  URI: memory://hello/template
  Text: Hello, {name}! This greeting was served by a native Chio service.

Prompt:
  First message: Compose a short, polite greeting for Ada.

Late events:
  Count: 1

=== done ===

Walkthrough

Building the service

NativeChioServiceBuilder::new(server_id, public_key_hex) is the entry point, imported from chio-mcp-adapter's native module. The server id binds the service to capability grants; the public key hex goes into the signed manifest. build_service returns a Result because the closing .build() validates the manifest.

examples/hello-tool/src/lib.rs
use chio_core::crypto::Keypair;
use chio_core::{PromptMessage, ResourceContent};
use chio_kernel::{
    KernelError, PromptProvider, ResourceProvider, ToolServerConnection, ToolServerEvent,
};
use chio_mcp_adapter::native::{
    NativeChioService, NativeChioServiceBuilder, NativePrompt, NativeResource, NativeTool,
};

pub fn build_service(
    public_key_hex: String,
) -> Result<NativeChioService, chio_manifest::ManifestError> {
    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",
        )

Registering the tool

NativeTool::new takes the tool name, description, and JSON Schema for the input. The builder attaches an output schema, marks it read_only(), advertises a per-invocation price, and registers a closure that runs when the tool is invoked. The closure calls a small greeting_name helper that returns KernelError::RequestIncomplete when name is missing, so the tool fails closed on malformed input.

examples/hello-tool/src/lib.rs
        .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 = greeting_name(&arguments)?;
                Ok(serde_json::json!({
                    "greeting": format!("Hello, {name}! This greeting was served by a native Chio service.")
                }))
            },
        )

Resource and prompt

Static resources and prompts are registered the same way: declare the descriptor, hand the builder the static content. The kernel will return them verbatim on read.

examples/hello-tool/src/lib.rs
        .static_resource(
            NativeResource::new("memory://hello/template", "Greeting Template")
                .description("A static greeting template used by the hello example")
                .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 user 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()
}

Signing the manifest

The example flow lives in pub async fn run(). It generates a keypair, builds the service, prints the manifest, and signs it. The signed manifest is what consumers (an edge, a kernel, or an authority) verify when issuing capabilities.

examples/hello-tool/src/lib.rs
pub async fn run() -> HelloToolResult<()> {
    let server_kp = Keypair::generate();
    let service = build_service(server_kp.public_key().to_hex())?;

    // ... print the manifest and its pricing ...

    chio_manifest::sign_manifest(service.manifest(), &server_kp)?;
    println!("Manifest signed successfully.");

Invoking and draining events

service.invoke(tool_name, args, nested_flow) runs the registered closure. It is an async fn, awaited inside run(); so is drain_events. read_resource and get_prompt are synchronous accessors that return an Option. emit_event queues late notifications (e.g. ResourcesListChanged); drain_events returns and clears the queue.

examples/hello-tool/src/lib.rs
    let greeting = service
        .invoke("greet", serde_json::json!({ "name": "World" }), None)
        .await?;

    let resource = service
        .read_resource("memory://hello/template")?
        .ok_or_else(|| {
            KernelError::ToolServerError("hello template resource missing".to_string())
        })?;

    let prompt = service
        .get_prompt("compose_greeting", serde_json::json!({}))?
        .ok_or_else(|| {
            KernelError::ToolServerError("compose_greeting prompt missing".to_string())
        })?;

    service.emit_event(ToolServerEvent::ResourcesListChanged);
    let events = service.drain_events().await?;

The binary wrapper

src/main.rs is an eleven-line wrapper. It gives the async run() a Tokio runtime and map a failure onto a non-zero exit code. All the logic above lives in src/lib.rs, which keeps it reachable from the tests.

examples/hello-tool/src/main.rs
use std::process::ExitCode;

#[tokio::main]
async fn main() -> ExitCode {
    if let Err(error) = hello_tool::run().await {
        eprintln!("{error}");
        return ExitCode::FAILURE;
    }

    ExitCode::SUCCESS
}

Success criteria

The example has no smoke.sh; the embedded tests in src/lib.rs are what assert the service is wired correctly. Run them with cargo test -p hello-tool. The pricing test asserts these four facts about the tool manifest:

examples/hello-tool/src/lib.rs
const TEST_PUBLIC_KEY: &str =
    "7b0f6f631f6e66207140ead0b6b2e9418916d2c4b3c7448ba5f7ed27f5c8d038";

#[test]
fn hello_tool_manifest_advertises_pricing_metadata() -> HelloToolResult<()> {
    let service = build_service(TEST_PUBLIC_KEY.to_string())?;
    let tool = &service.manifest().tools[0];

    assert_eq!(tool.name, "greet");
    assert_eq!(
        tool.pricing.as_ref().map(|pricing| pricing.pricing_model),
        Some(PricingModel::PerInvocation)
    );
    assert_eq!(
        tool.pricing
            .as_ref()
            .and_then(|pricing| pricing.unit_price.as_ref())
            .map(|amount| (amount.units, amount.currency.as_str())),
        Some((25, "USD"))
    );
    assert_eq!(
        tool.pricing
            .as_ref()
            .and_then(|pricing| pricing.billing_unit.as_deref()),
        Some("invocation")
    );
    Ok(())
}
  • The first tool in the manifest is named greet.
  • Pricing model is PricingModel::PerInvocation.
  • Unit price is 25 in USD.
  • Billing unit is the string invocation.

src/lib.rs also carries two async tests: one drives invoke, the resource, the prompt, and event draining; the other asserts that greet fails closed when the required name field is absent.

For the binary itself, the success criteria are the printed output: Manifest signed successfully. followed by an Output: line containing Hello, World! and a Late events: Count: 1 line. The process exits with status 0.


Inspect after

After cargo run exits cleanly, confirm the service value is shaped correctly with these in-process commands. Add them to your own driver if you want to script verification.

bash
# Run the binary and capture stdout
cargo run -p hello-tool > hello-tool.out 2>&1

# 1. The manifest signs (line near the top of the output)
grep -F "Manifest signed successfully." hello-tool.out
# expected: Manifest signed successfully.

# 2. The greet tool returned the deterministic payload
grep -F 'Hello, World!' hello-tool.out
# expected: ...Hello, World! This greeting was served by a native Chio service.

# 3. The static resource read returned the template
grep -F "memory://hello/template" hello-tool.out
# expected: URI: memory://hello/template

# 4. The late event drained
grep -F "Count: 1" hello-tool.out
# expected: Count: 1

For programmatic checks against the manifest, copy the test pattern above: build the service, then assert on service.manifest(). Useful fields to check at runtime:

  • service.manifest().name "Hello Tool Server"
  • service.server_id() "srv-hello"
  • service.manifest().tools.len() 1
  • service.invoke("greet", json!({"name":"World"}), None).await? → JSON object with "greeting" field.

Pricing is advisory

The advertised price is metadata for budget planners. The enforced limit comes from the capability grant's max_cost_per_invocation and max_total_cost. The example flow is:

  1. Inspect tool pricing from the signed manifest.
  2. Choose a safe per-call ceiling and total budget.
  3. Issue a capability whose monetary budget matches that quote.

Next