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
NativeChioServiceBuilderfromchio-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 USDper invocation) for pre-call budget planning. - Late event emission via
emit_event/drain_events.
Why native and not wrapped MCP
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
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:
[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
cd examples/hello-tool
cargo runThe binary prints the manifest, signs it, invokes greet, reads the resource, gets the prompt, and drains a late event. Output looks like:
=== 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.
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.
.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.
.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.
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.
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:
#[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
25inUSD. - 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.
# 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: 1For 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()→1service.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:
- Inspect tool pricing from the signed manifest.
- Choose a safe per-call ceiling and total budget.
- Issue a capability whose monetary budget matches that quote.
Next
- Hello MCP: the matching protocol-edge example over stdio JSON-RPC.
- Native Tool Server: the longer guide that motivated the migration from wrapped MCP.
- Wrap an MCP Server: the wrapped-adapter starting point this example replaces.
- Examples Overview: the full index.