Skip to main content

CallToolResult

Struct CallToolResult 

Source
#[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
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional 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: bool

Whether 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 (ChatGPT manages this)
  • Display hints: colors, animations, layout preferences
  • Internal IDs that the model doesn’t need

Implementations§

Source§

impl CallToolResult

Source

pub fn new(content: Vec<Content>) -> Self

Create a new tool result with content.

Source

pub fn error(content: Vec<Content>) -> Self

Create an error result.

Source

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.

Source

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);
Source

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 })));
Source

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"#));
Source

pub fn with_structured_content(self, content: Value) -> Self

Add structured content for both model and widget.

Source

pub fn with_meta(self, meta: Map<String, Value>) -> Self

Add widget-only metadata.

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");
Source

pub fn related_task(&self) -> Option<TaskMetadata>

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());
Source

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.

Trait Implementations§

Source§

impl Clone for CallToolResult

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for CallToolResult

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for CallToolResult

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for CallToolResult

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for CallToolResult

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more