Skip to main content

Content Types

MCP tool and resource handlers can return several different kinds of content — not just plain text. This page covers all available content types and when to use each.

Tool Response Types

Tool handlers can return any type that converts into CallToolResponse. The most common ones:

Text

Returning a String or &str from a #[tool] handler produces a text response automatically:

#[tool(descr = "Greets a user")]
async fn greet(name: String) -> String {
format!("Hello, {name}!")
}

Structured JSON

To return a serializable type as structured output, wrap it in Json<T> and use it as the return type:

use neva::prelude::*;
use serde::Serialize;

#[derive(Serialize)]
struct WeatherReport {
city: String,
temperature_c: f64,
condition: String,
}

#[tool(descr = "Returns weather for a city")]
async fn get_weather(city: String) -> Json<WeatherReport> {
WeatherReport {
city,
temperature_c: 22.5,
condition: "Sunny".into(),
}
.into()
}

Json<T> automatically converts into a CallToolResponse with structured content. You can also construct it explicitly with Json(value) or call CallToolResponse::json(value) when building the response manually.

Images

Return binary image data using Content::image(). The data must be base64-encoded bytes:

use neva::prelude::*;

#[tool(descr = "Returns a chart image")]
async fn generate_chart(data: String) -> CallToolResponse {
let png_bytes = render_chart(&data); // your rendering logic

let image = Content::image(png_bytes)
.with_mime("image/png");

CallToolResponse::from(image)
}

Default MIME type is image/jpg. Use .with_mime() to set a different type.

Audio

Return audio data the same way using Content::audio():

#[tool(descr = "Returns synthesized speech")]
async fn synthesize(text: String) -> CallToolResponse {
let wav_bytes = text_to_speech(&text); // your TTS logic

let audio = Content::audio(wav_bytes)
.with_mime("audio/wav");

CallToolResponse::from(audio)
}

Default MIME type is audio/wav.

Multiple Content Items

Use CallToolResponse::array() to return several content items in a single response:

use neva::prelude::*;

#[tool(descr = "Returns analysis with a chart")]
async fn analyze(data: String) -> CallToolResponse {
let summary = Content::text(format!("Analysis of: {data}"));
let chart = Content::image(render_chart(&data)).with_mime("image/png");

CallToolResponse::array([summary, chart])
}

Reference an external resource without embedding it:

use neva::prelude::*;

#[tool(descr = "Returns a link to the report")]
async fn get_report_link(id: String) -> CallToolResponse {
let link = Content::from(
ResourceLink::from(format!("res://reports/{id}"))
);
CallToolResponse::from(link)
}

Embedded Resources

Embed a resource inline in the tool response:

use neva::prelude::*;

#[tool(descr = "Returns report contents inline")]
async fn get_report_inline(id: String) -> CallToolResponse {
let uri: Uri = format!("res://reports/{id}").parse().unwrap();
let contents = ResourceContents::new(uri.clone())
.with_text("Report contents here...");

let embedded = Content::from(EmbeddedResource::from(contents));
CallToolResponse::from(embedded)
}

Error Responses

Signal a tool-level error that the model can read and reason about. Unlike Rust errors (which abort the request), tool errors are returned as structured content:

#[tool(descr = "Fetches a record")]
async fn fetch_record(id: String) -> CallToolResponse {
match load_from_db(&id).await {
Ok(record) => CallToolResponse::json(record),
Err(e) => CallToolResponse::error(e.to_string()),
}
}

See Error Handling for the difference between tool errors and request-level errors.


Resource Content Types

Resource handlers return ResourceContents, which can be text, JSON, or binary.

Text

#[resource(uri = "note://{id}", title = "Read note")]
async fn get_note(uri: Uri, id: String) -> ResourceContents {
let text = load_note(&id).await;
ResourceContents::new(uri).with_text(text)
}

JSON

use serde_json::json;

#[resource(uri = "config://{key}", title = "Read config")]
async fn get_config(uri: Uri, key: String) -> ResourceContents {
let value = json!({ "key": key, "enabled": true });
ResourceContents::new(uri).with_json(value)
}

Binary (Blob)

#[resource(uri = "file://{path}", title = "Read file", mime = "application/octet-stream")]
async fn get_file(uri: Uri, path: String) -> ResourceContents {
let bytes = std::fs::read(&path).unwrap_or_default();
ResourceContents::new(uri).with_blob(bytes)
}

Shorthand Return Types

Neva implements Into<CallToolResponse> for many common types, so you often don't need to construct CallToolResponse manually:

Return typeEquivalent to
StringCallToolResponse::new(text)
&strCallToolResponse::new(text)
Json<T>CallToolResponse::json(value)
(String, String)Two-field text tuple (uri, body)
Vec<Content>CallToolResponse::array(items)
ContentSingle-item response

Similarly for ResourceContents:

Return typeEquivalent to
StringResourceContents::new(uri).with_text(s)
(String, String)URI + text body
serde_json::ValueResourceContents::new(uri).with_json(v)