Chio/Docs

Hello Tool

The smallest native Chio service. No MCP wrapping, no HTTP framework, no sidecar. Just a Rust binary that builds a NativeChioService, signs its manifest, and invokes its own tool through the kernel.

What it shows

  • A native service built with NativeChioServiceBuilder from chio-mcp-adapter.
  • 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.

Why native and not wrapped MCP

The repo already has strong wrapped-MCP support through chio mcp serve. This example is the next step: same policy and trust model, but the subprocess is replaced with a native service value. See Native Tool Server for the full migration path.

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 crates from the workspace:

examples/hello-tool/Cargo.toml
[dependencies]
chio-core = { package = "chio-core-types", path = "../../crates/chio-core-types" }
chio-kernel = { package = "chio-kernel", path = "../../crates/chio-kernel" }
chio-manifest = { package = "chio-manifest", path = "../../crates/chio-manifest" }
chio-mcp-adapter = { package = "chio-mcp-adapter", path = "../../crates/chio-mcp-adapter" }
serde = { workspace = true }
serde_json = { 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. The server id binds the service to capability grants; the public key hex goes into the signed manifest.

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

fn build_service(public_key_hex: String) -> chio_mcp_adapter::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",
        )

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.

examples/hello-tool/src/main.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 = 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.")
                }))
            },
        )

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/main.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()
        .expect("build native Chio service")
}

Signing the manifest

After building the service, the example generates a keypair and signs the manifest. The signed manifest is what consumers (an edge, a kernel, or an authority) verify when issuing capabilities.

examples/hello-tool/src/main.rs
fn main() {
    let server_kp = Keypair::generate();
    let service = build_service(server_kp.public_key().to_hex());

    match chio_manifest::sign_manifest(service.manifest(), &server_kp) {
        Ok(_signed) => println!("Manifest signed successfully."),
        Err(error) => {
            eprintln!("Failed to sign manifest: {error}");
            std::process::exit(1);
        }
    }

Invoking and draining events

service.invoke(tool_name, args, nested_flow) runs the registered closure. Resources and prompts have their own accessors. emit_event queues late notifications (e.g. ResourcesListChanged); drain_events returns and clears the queue.

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

    let resource = service
        .read_resource("memory://hello/template")
        .expect("read resource")
        .expect("resource exists");

    let prompt = service
        .get_prompt("compose_greeting", serde_json::json!({}))
        .expect("get prompt")
        .expect("prompt exists");

    service.emit_event(ToolServerEvent::ResourcesListChanged);
    let events = service.drain_events().expect("drain events");

Success criteria

The example has no smoke.sh; the embedded test in main.rs is what asserts the service is wired correctly. Run it with cargo test -p hello-tool. The test asserts these four facts about the signed manifest:

examples/hello-tool/src/main.rs
#[test]
fn hello_tool_manifest_advertises_pricing_metadata() {
    let service = build_service(
        "7b0f6f631f6e66207140ead0b6b2e9418916d2c4b3c7448ba5f7ed27f5c8d038".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")
    );
}
  • 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.

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)? → JSON object with "greeting" field.

Pricing is advisory

The advertised price is metadata for budget planners. The actual hard stop 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