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
NativeChioServiceBuilderfromchio-mcp-adapter'snativemodule. - 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.
Native service and wrapped MCP
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
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):
[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
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, 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.
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.
.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.
.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.
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.
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.
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:
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
25inUSD. - 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.
# 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).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:
- 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.