#[non_exhaustive]pub struct CallToolResult {
pub content: Vec<Content>,
pub is_error: bool,
pub structured_content: Option<Value>,
pub _meta: Option<Map<String, Value>>,
}Expand description
Tool call result.
Supports three-tier response model for MCP Apps:
content: Model-focused narration (goes to model, optionally to widget)structured_content: Structured data for both model and widget_meta: Widget-only metadata (never sent to model)
§ChatGPT Apps Example
use pmcp::types::CallToolResult;
use serde_json::json;
let result = CallToolResult::new(vec![])
.with_structured_content(json!({
"boardState": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR",
"lastMove": { "from": "e2", "to": "e4" }
}))
.with_meta(json!({
"widgetState": { "selectedSquare": null }
}).as_object().unwrap().clone());§Backward Compatibility
Use constructors for clean, future-proof initialization:
use pmcp::types::{CallToolResult, Content};
let result = CallToolResult::new(vec![Content::text("Hello")]);
assert!(!result.is_error);
let error = CallToolResult::error(vec![Content::text("Something went wrong")]);
assert!(error.is_error);Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.content: Vec<Content>Tool execution result (model-focused narration).
This content is primarily for the model to understand the result.
In ChatGPT Apps, this appears as text below the widget.
is_error: boolWhether the tool call represents an error.
structured_content: Option<Value>Structured data for both model and widget (ChatGPT Apps / MCP Apps Extension).
Use this for data that should be accessible to both the AI model (for reasoning) and the widget (for display). Examples:
- Game board state (chess position, game score)
- Query results (database rows, search results)
- Form data (user selections, validated input)
§Era: what shape is allowed
The 2026-07-28 schema declares this field as structuredContent?: unknown
— “An optional JSON value that represents the structured result of the
tool call. This can be any JSON value (object, array, string, number, boolean, or null)
that conforms to the tool’s outputSchema if one is defined.”
(CallToolResult in the vendored schema/vendored/core-2026-07-28/schema.ts.)
The 2025-11-25 (v1) schema text was narrower on BOTH halves:
structuredContent?: { [key: string]: unknown }, and outputSchema was
“Currently restricted to type: "object" at the root level”. v2 lifts
both restrictions.
CallToolResult::structured_value is the
constructor that names a non-object payload; use it rather than
structured so the choice is greppable at the call
site.
§pmcp’s v1 permissiveness is FROZEN here, not corrected
This field has always been Option<Value>, and neither native dispatcher
(ServerCore in src/server/core.rs, the high-level Server in
src/server/mod.rs) shape-checks the handler’s value on the way out — so
pmcp already emits non-object structuredContent on v1 today, which is
more permissive than v1’s own spec text allows. Phase 115 decision D-05
freezes v1 behaviour byte-identically, so that over-permissiveness is
FROZEN rather than fixed: tightening v1 to reject scalars would ITSELF be a
v1 wire change, and is forbidden. Do not add a shape guard here “for
correctness” — tests/structured_tool_output.rs fences the v1 half on both
dispatchers precisely so a later tightening fails loudly.
skip_serializing_if distinguishes the two absences that matter:
None omits the key entirely, while Some(Value::Null) emits an explicit
"structuredContent": null — a value v2 permits.
_meta: Option<Map<String, Value>>Widget-only metadata (ChatGPT Apps / MCP Apps Extension).
Metadata that goes only to the widget, never to the model. Use for widget display hints, UI state, and internal widget data. Examples:
widgetState: Persisted widget state (ChatGPTmanages this)- Display hints: colors, animations, layout preferences
- Internal IDs that the model doesn’t need
Implementations§
Source§impl CallToolResult
impl CallToolResult
Sourcepub fn rejected(message: impl Into<String>, details: Option<Value>) -> Self
pub fn rejected(message: impl Into<String>, details: Option<Value>) -> Self
Create a tool-level rejection result.
An isError: true result whose content is the model-readable
message and whose structuredContent is details (when present).
This is the envelope the server’s tools/call dispatch produces for
Error::ToolRejected — an application
rejection the caller should correct and retry, NOT a protocol fault.
Sourcepub fn structured(value: Value) -> Self
pub fn structured(value: Value) -> Self
Create a structured success result — the success-side counterpart of
rejected.
One value, one call, both voices: structuredContent carries value
verbatim for structured-aware clients, and content carries the
canonical JSON serialization of the same value as text, so text-only
clients (older hosts, log pipelines) keep working. Per the MCP spec, a
tool that declares an outputSchema SHOULD return structuredContent
conforming to it — this constructor is the one-call way to do that
from handlers that own their CallToolResult envelope.
Use structured_with_text when the
human-readable voice should differ from the raw serialization, and
structured_value when the payload is NOT an
object (a scalar, an array or null) — this constructor keeps its
object-shaped intent so every existing call site reads the same way.
§Example
use pmcp::types::CallToolResult;
use serde_json::json;
let value = json!({ "rows": [1, 2, 3] });
let result = CallToolResult::structured(value.clone());
assert!(!result.is_error);
assert_eq!(result.structured_content, Some(value.clone()));
// The text voice round-trips to the same value.
let pmcp::types::Content::Text { text } = &result.content[0] else {
unreachable!()
};
assert_eq!(serde_json::from_str::<serde_json::Value>(text).unwrap(), value);Sourcepub fn structured_with_text(value: Value, text: impl Into<String>) -> Self
pub fn structured_with_text(value: Value, text: impl Into<String>) -> Self
Create a structured success result with a distinct human-readable voice.
Like structured, but content carries text
instead of the raw JSON serialization — mirroring the two-voice
separation rejected has on the error side.
§Example
use pmcp::types::CallToolResult;
use serde_json::json;
let result = CallToolResult::structured_with_text(
json!({ "matches": 42 }),
"Found 42 matches.",
);
assert_eq!(result.structured_content, Some(json!({ "matches": 42 })));Sourcepub fn structured_value(value: Value) -> Self
pub fn structured_value(value: Value) -> Self
Create a structured success result whose payload is NOT a JSON object —
the widening sibling of structured.
The body is identical to structured; the two differ
only in what they SAY. Reaching for this name is the deliberate, greppable
record at the call site that the payload is a scalar, an array or null
rather than the object shape most tools return. structured
keeps its exact signature and its object-shaped intent, so every existing
call site compiles and behaves identically (Phase 115 decision D-06).
§Era
The 2026-07-28 schema declares structuredContent?: unknown — “An
optional JSON value that represents the structured result of the tool call.
This can be any JSON value (object, array, string, number, boolean, or null)
that conforms to the tool’s outputSchema if one is defined.”
The 2025-11-25 schema text restricted it to
{ [key: string]: unknown }. pmcp’s v1 wire behaviour is FROZEN as-is
rather than tightened — see the note on
structured_content.
§A declared outputSchema still applies (D-04)
Widening the payload does not weaken the contract: if the tool declares an
outputSchema, that schema must DESCRIBE the scalar. {"type": "integer"} accepts 42; an object-shaped schema such as
{"type": "object", "required": ["n"]} does not.
“Does not accept” here means a tracing warning is logged at emit
time — src/server/output_validation.rs is warn-only on BOTH eras, so a
mismatch never turns the call into an error result and never adds a
production failure mode. The tool call still succeeds and the value still
reaches the wire.
§Some(null) is not None
A null payload is a PRESENT value: it serializes as an explicit
"structuredContent": null, whereas a result that never set the field
omits the key entirely.
§Example
use pmcp::types::CallToolResult;
use serde_json::json;
// A tool whose declared outputSchema is `{"type": "integer"}`.
let result = CallToolResult::structured_value(json!(42));
assert!(!result.is_error);
assert_eq!(result.structured_content, Some(json!(42)));
// The text voice carries the same value, for text-only clients.
let pmcp::types::Content::Text { text } = &result.content[0] else {
unreachable!()
};
assert_eq!(text, "42");
// A null payload is present, not absent.
let null_result = CallToolResult::structured_value(json!(null));
assert_eq!(null_result.structured_content, Some(json!(null)));
let wire = serde_json::to_string(&null_result).unwrap();
assert!(wire.contains(r#""structuredContent":null"#));Sourcepub fn with_structured_content(self, content: Value) -> Self
pub fn with_structured_content(self, content: Value) -> Self
Add structured content for both model and widget.
Attach related-task metadata under
RELATED_TASK_META_KEY (SEP-1686).
This is the server-emit twin of CallToolResult::related_task: it
records a TaskMetadata into
_meta so a client can recover it and
drive Client::wait_for_task without
hand-reading _meta. Existing _meta entries are preserved.
§Example
use pmcp::types::CallToolResult;
use pmcp::types::tasks::TaskMetadata;
let meta = TaskMetadata::new("t9").with_poll_interval(1000);
let result = CallToolResult::new(vec![]).with_related_task(meta);
assert_eq!(result.related_task().unwrap().task_id, "t9");Read related-task metadata from _meta under
RELATED_TASK_META_KEY.
Returns Some(TaskMetadata) when the result carries a well-formed
related-task entry, None when _meta is absent, the key is missing,
or the value does not deserialize (tamper-tolerant: never panics). The
minimal native shape { "taskId": "t9" } yields Some with the poll
fields defaulting to None.
§Example
use pmcp::types::CallToolResult;
let result = CallToolResult::new(vec![]);
assert!(result.related_task().is_none());Sourcepub fn with_widget_enrichment(
self,
info: &ToolInfo,
structured_value: Value,
) -> Self
pub fn with_widget_enrichment( self, info: &ToolInfo, structured_value: Value, ) -> Self
Enrich with widget metadata from a ToolInfo if it has widget meta.
Sets structured_content and _meta so widgets can access tool
output data. No-op for non-widget tools. Only clones _meta when
the tool actually has widget metadata.