feat(rmcp): add optional _meta to CallToolResult, EmbeddedResource, and ResourceContents (#386)
* feat(rmcp): support optional _meta on EmbeddedResource and ResourceContents - Add optional _meta to RawEmbeddedResource and ResourceContents (text/blob) - Update constructors/usages to set meta: None by default - Add tests to lock behavior (embedded text/blob + CallToolResult) - Update JSON Schemas to include optional _meta fields * style(rmcp): rustfmt after _meta changes and tests
This commit is contained in:
parent
1a20f8a0d1
commit
7d46b39b49
11 changed files with 311 additions and 3 deletions
|
|
@ -1267,6 +1267,9 @@ pub struct CallToolResult {
|
||||||
/// Whether this result represents an error condition
|
/// Whether this result represents an error condition
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub is_error: Option<bool>,
|
pub is_error: Option<bool>,
|
||||||
|
/// Optional protocol-level metadata for this result
|
||||||
|
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub meta: Option<Meta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CallToolResult {
|
impl CallToolResult {
|
||||||
|
|
@ -1276,6 +1279,7 @@ impl CallToolResult {
|
||||||
content,
|
content,
|
||||||
structured_content: None,
|
structured_content: None,
|
||||||
is_error: Some(false),
|
is_error: Some(false),
|
||||||
|
meta: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Create an error tool result with unstructured content
|
/// Create an error tool result with unstructured content
|
||||||
|
|
@ -1284,6 +1288,7 @@ impl CallToolResult {
|
||||||
content,
|
content,
|
||||||
structured_content: None,
|
structured_content: None,
|
||||||
is_error: Some(true),
|
is_error: Some(true),
|
||||||
|
meta: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Create a successful tool result with structured content
|
/// Create a successful tool result with structured content
|
||||||
|
|
@ -1305,6 +1310,7 @@ impl CallToolResult {
|
||||||
content: vec![Content::text(value.to_string())],
|
content: vec![Content::text(value.to_string())],
|
||||||
structured_content: Some(value),
|
structured_content: Some(value),
|
||||||
is_error: Some(false),
|
is_error: Some(false),
|
||||||
|
meta: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Create an error tool result with structured content
|
/// Create an error tool result with structured content
|
||||||
|
|
@ -1330,6 +1336,7 @@ impl CallToolResult {
|
||||||
content: vec![Content::text(value.to_string())],
|
content: vec![Content::text(value.to_string())],
|
||||||
structured_content: Some(value),
|
structured_content: Some(value),
|
||||||
is_error: Some(true),
|
is_error: Some(true),
|
||||||
|
meta: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1377,6 +1384,9 @@ impl<'de> Deserialize<'de> for CallToolResult {
|
||||||
structured_content: Option<Value>,
|
structured_content: Option<Value>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
is_error: Option<bool>,
|
is_error: Option<bool>,
|
||||||
|
/// Accept `_meta` during deserialization
|
||||||
|
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||||
|
meta: Option<Meta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let helper = CallToolResultHelper::deserialize(deserializer)?;
|
let helper = CallToolResultHelper::deserialize(deserializer)?;
|
||||||
|
|
@ -1384,6 +1394,7 @@ impl<'de> Deserialize<'de> for CallToolResult {
|
||||||
content: helper.content.unwrap_or_default(),
|
content: helper.content.unwrap_or_default(),
|
||||||
structured_content: helper.structured_content,
|
structured_content: helper.structured_content,
|
||||||
is_error: helper.is_error,
|
is_error: helper.is_error,
|
||||||
|
meta: helper.meta,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate mutual exclusivity
|
// Validate mutual exclusivity
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ pub type ImageContent = Annotated<RawImageContent>;
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct RawEmbeddedResource {
|
pub struct RawEmbeddedResource {
|
||||||
|
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub meta: Option<super::Meta>,
|
||||||
pub resource: ResourceContents,
|
pub resource: ResourceContents,
|
||||||
}
|
}
|
||||||
pub type EmbeddedResource = Annotated<RawEmbeddedResource>;
|
pub type EmbeddedResource = Annotated<RawEmbeddedResource>;
|
||||||
|
|
@ -88,15 +90,20 @@ impl RawContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resource(resource: ResourceContents) -> Self {
|
pub fn resource(resource: ResourceContents) -> Self {
|
||||||
RawContent::Resource(RawEmbeddedResource { resource })
|
RawContent::Resource(RawEmbeddedResource {
|
||||||
|
meta: None,
|
||||||
|
resource,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
|
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
|
||||||
RawContent::Resource(RawEmbeddedResource {
|
RawContent::Resource(RawEmbeddedResource {
|
||||||
|
meta: None,
|
||||||
resource: ResourceContents::TextResourceContents {
|
resource: ResourceContents::TextResourceContents {
|
||||||
uri: uri.into(),
|
uri: uri.into(),
|
||||||
mime_type: Some("text".to_string()),
|
mime_type: Some("text".to_string()),
|
||||||
text: content.into(),
|
text: content.into(),
|
||||||
|
meta: None,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,8 @@ variant_extension! {
|
||||||
PromptListChangedNotification
|
PromptListChangedNotification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Meta(pub JsonObject);
|
pub struct Meta(pub JsonObject);
|
||||||
const PROGRESS_TOKEN_FIELD: &str = "progressToken";
|
const PROGRESS_TOKEN_FIELD: &str = "progressToken";
|
||||||
|
|
|
||||||
|
|
@ -149,12 +149,14 @@ impl PromptMessage {
|
||||||
uri,
|
uri,
|
||||||
mime_type: Some(mime_type),
|
mime_type: Some(mime_type),
|
||||||
text: text.unwrap_or_default(),
|
text: text.unwrap_or_default(),
|
||||||
|
meta: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
role,
|
role,
|
||||||
content: PromptMessageContent::Resource {
|
content: PromptMessageContent::Resource {
|
||||||
resource: RawEmbeddedResource {
|
resource: RawEmbeddedResource {
|
||||||
|
meta: None,
|
||||||
resource: resource_contents,
|
resource: resource_contents,
|
||||||
}
|
}
|
||||||
.optional_annotate(annotations),
|
.optional_annotate(annotations),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::Annotated;
|
use super::{Annotated, Meta};
|
||||||
|
|
||||||
/// Represents a resource in the extension with metadata
|
/// Represents a resource in the extension with metadata
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
|
|
@ -51,6 +51,8 @@ pub enum ResourceContents {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
mime_type: Option<String>,
|
mime_type: Option<String>,
|
||||||
text: String,
|
text: String,
|
||||||
|
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||||
|
meta: Option<Meta>,
|
||||||
},
|
},
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
BlobResourceContents {
|
BlobResourceContents {
|
||||||
|
|
@ -58,6 +60,8 @@ pub enum ResourceContents {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
mime_type: Option<String>,
|
mime_type: Option<String>,
|
||||||
blob: String,
|
blob: String,
|
||||||
|
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||||
|
meta: Option<Meta>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,6 +71,7 @@ impl ResourceContents {
|
||||||
uri: uri.into(),
|
uri: uri.into(),
|
||||||
mime_type: Some("text".into()),
|
mime_type: Some("text".into()),
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
|
meta: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +119,7 @@ mod tests {
|
||||||
uri: "file:///test.txt".to_string(),
|
uri: "file:///test.txt".to_string(),
|
||||||
mime_type: Some("text/plain".to_string()),
|
mime_type: Some("text/plain".to_string()),
|
||||||
text: "Hello world".to_string(),
|
text: "Hello world".to_string(),
|
||||||
|
meta: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&text_contents).unwrap();
|
let json = serde_json::to_string(&text_contents).unwrap();
|
||||||
|
|
|
||||||
122
crates/rmcp/tests/test_embedded_resource_meta.rs
Normal file
122
crates/rmcp/tests/test_embedded_resource_meta.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
use rmcp::model::{AnnotateAble, Content, Meta, RawContent, ResourceContents};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_embedded_text_resource_with_meta() {
|
||||||
|
// Inner contents meta
|
||||||
|
let mut inner_meta = Meta::new();
|
||||||
|
inner_meta.insert("inner".to_string(), json!(2));
|
||||||
|
|
||||||
|
// Top-level embedded resource meta
|
||||||
|
let mut top_meta = Meta::new();
|
||||||
|
top_meta.insert("top".to_string(), json!(1));
|
||||||
|
|
||||||
|
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
|
||||||
|
meta: Some(top_meta),
|
||||||
|
resource: ResourceContents::TextResourceContents {
|
||||||
|
uri: "str://example".to_string(),
|
||||||
|
mime_type: Some("text/plain".to_string()),
|
||||||
|
text: "hello".to_string(),
|
||||||
|
meta: Some(inner_meta),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.no_annotation();
|
||||||
|
|
||||||
|
let v = serde_json::to_value(&content).unwrap();
|
||||||
|
|
||||||
|
let expected = json!({
|
||||||
|
"type": "resource",
|
||||||
|
"_meta": {"top": 1},
|
||||||
|
"resource": {
|
||||||
|
"uri": "str://example",
|
||||||
|
"mimeType": "text/plain",
|
||||||
|
"text": "hello",
|
||||||
|
"_meta": {"inner": 2}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(v, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_embedded_text_resource_without_meta_omits_fields() {
|
||||||
|
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
|
||||||
|
meta: None,
|
||||||
|
resource: ResourceContents::TextResourceContents {
|
||||||
|
uri: "str://no-meta".to_string(),
|
||||||
|
mime_type: Some("text/plain".to_string()),
|
||||||
|
text: "hi".to_string(),
|
||||||
|
meta: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.no_annotation();
|
||||||
|
|
||||||
|
let v = serde_json::to_value(&content).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(v.get("_meta"), None);
|
||||||
|
let inner = v.get("resource").and_then(|r| r.as_object()).unwrap();
|
||||||
|
assert_eq!(inner.get("_meta"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_embedded_text_resource_with_meta() {
|
||||||
|
let raw = json!({
|
||||||
|
"type": "resource",
|
||||||
|
"_meta": {"x": true},
|
||||||
|
"resource": {
|
||||||
|
"uri": "str://from-json",
|
||||||
|
"text": "ok",
|
||||||
|
"_meta": {"y": 42}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let content: Content = serde_json::from_value(raw).unwrap();
|
||||||
|
|
||||||
|
let raw = match &content.raw {
|
||||||
|
RawContent::Resource(er) => er,
|
||||||
|
_ => panic!("expected resource"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// top-level _meta
|
||||||
|
let top = raw.meta.as_ref().expect("top-level meta missing");
|
||||||
|
assert_eq!(top.get("x").unwrap(), &json!(true));
|
||||||
|
|
||||||
|
// inner contents _meta
|
||||||
|
match &raw.resource {
|
||||||
|
ResourceContents::TextResourceContents {
|
||||||
|
meta, uri, text, ..
|
||||||
|
} => {
|
||||||
|
assert_eq!(uri, "str://from-json");
|
||||||
|
assert_eq!(text, "ok");
|
||||||
|
let inner = meta.as_ref().expect("inner meta missing");
|
||||||
|
assert_eq!(inner.get("y").unwrap(), &json!(42));
|
||||||
|
}
|
||||||
|
_ => panic!("expected text resource contents"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_embedded_blob_resource_with_meta() {
|
||||||
|
let mut inner_meta = Meta::new();
|
||||||
|
inner_meta.insert("blob_inner".to_string(), json!(true));
|
||||||
|
|
||||||
|
let mut top_meta = Meta::new();
|
||||||
|
top_meta.insert("blob_top".to_string(), json!("t"));
|
||||||
|
|
||||||
|
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
|
||||||
|
meta: Some(top_meta),
|
||||||
|
resource: ResourceContents::BlobResourceContents {
|
||||||
|
uri: "str://blob".to_string(),
|
||||||
|
mime_type: Some("application/octet-stream".to_string()),
|
||||||
|
blob: "Zm9v".to_string(),
|
||||||
|
meta: Some(inner_meta),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.no_annotation();
|
||||||
|
|
||||||
|
let v = serde_json::to_value(&content).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(v.get("_meta").unwrap(), &json!({"blob_top": "t"}));
|
||||||
|
let inner = v.get("resource").unwrap();
|
||||||
|
assert_eq!(inner.get("_meta").unwrap(), &json!({"blob_inner": true}));
|
||||||
|
}
|
||||||
|
|
@ -892,6 +892,13 @@
|
||||||
"RawEmbeddedResource": {
|
"RawEmbeddedResource": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"resource": {
|
"resource": {
|
||||||
"$ref": "#/definitions/ResourceContents"
|
"$ref": "#/definitions/ResourceContents"
|
||||||
}
|
}
|
||||||
|
|
@ -1252,6 +1259,13 @@
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"mimeType": {
|
"mimeType": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
|
|
@ -1273,6 +1287,13 @@
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"blob": {
|
"blob": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -892,6 +892,13 @@
|
||||||
"RawEmbeddedResource": {
|
"RawEmbeddedResource": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"resource": {
|
"resource": {
|
||||||
"$ref": "#/definitions/ResourceContents"
|
"$ref": "#/definitions/ResourceContents"
|
||||||
}
|
}
|
||||||
|
|
@ -1252,6 +1259,13 @@
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"mimeType": {
|
"mimeType": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
|
|
@ -1273,6 +1287,13 @@
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"blob": {
|
"blob": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,13 @@
|
||||||
"Annotated3": {
|
"Annotated3": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
|
@ -319,6 +326,14 @@
|
||||||
"description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.",
|
"description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"description": "Optional protocol-level metadata for this result",
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"description": "The content returned by the tool (text, images, etc.)",
|
"description": "The content returned by the tool (text, images, etc.)",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -1380,6 +1395,13 @@
|
||||||
"RawEmbeddedResource": {
|
"RawEmbeddedResource": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"resource": {
|
"resource": {
|
||||||
"$ref": "#/definitions/ResourceContents"
|
"$ref": "#/definitions/ResourceContents"
|
||||||
}
|
}
|
||||||
|
|
@ -1531,6 +1553,13 @@
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"mimeType": {
|
"mimeType": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
|
|
@ -1552,6 +1581,13 @@
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"blob": {
|
"blob": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,13 @@
|
||||||
"Annotated3": {
|
"Annotated3": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
|
@ -319,6 +326,14 @@
|
||||||
"description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.",
|
"description": "The result of a tool call operation.\n\nContains the content returned by the tool execution and an optional\nflag indicating whether the operation resulted in an error.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"description": "Optional protocol-level metadata for this result",
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"description": "The content returned by the tool (text, images, etc.)",
|
"description": "The content returned by the tool (text, images, etc.)",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -1380,6 +1395,13 @@
|
||||||
"RawEmbeddedResource": {
|
"RawEmbeddedResource": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"resource": {
|
"resource": {
|
||||||
"$ref": "#/definitions/ResourceContents"
|
"$ref": "#/definitions/ResourceContents"
|
||||||
}
|
}
|
||||||
|
|
@ -1531,6 +1553,13 @@
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"mimeType": {
|
"mimeType": {
|
||||||
"type": [
|
"type": [
|
||||||
"string",
|
"string",
|
||||||
|
|
@ -1552,6 +1581,13 @@
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"_meta": {
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
"blob": {
|
"blob": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
45
crates/rmcp/tests/test_tool_result_meta.rs
Normal file
45
crates/rmcp/tests/test_tool_result_meta.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use rmcp::model::{CallToolResult, Content, Meta};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_tool_result_with_meta() {
|
||||||
|
let content = vec![Content::text("ok")];
|
||||||
|
let mut meta = Meta::new();
|
||||||
|
meta.insert("foo".to_string(), json!("bar"));
|
||||||
|
let result = CallToolResult {
|
||||||
|
content,
|
||||||
|
structured_content: None,
|
||||||
|
is_error: Some(false),
|
||||||
|
meta: Some(meta),
|
||||||
|
};
|
||||||
|
let v = serde_json::to_value(&result).unwrap();
|
||||||
|
let expected = json!({
|
||||||
|
"content": [{"type":"text","text":"ok"}],
|
||||||
|
"isError": false,
|
||||||
|
"_meta": {"foo":"bar"}
|
||||||
|
});
|
||||||
|
assert_eq!(v, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_tool_result_with_meta() {
|
||||||
|
let raw: Value = json!({
|
||||||
|
"content": [{"type":"text","text":"hello"}],
|
||||||
|
"isError": true,
|
||||||
|
"_meta": {"a": 1, "b": "two"}
|
||||||
|
});
|
||||||
|
let result: CallToolResult = serde_json::from_value(raw).unwrap();
|
||||||
|
assert_eq!(result.is_error, Some(true));
|
||||||
|
assert_eq!(result.content.len(), 1);
|
||||||
|
let meta = result.meta.expect("meta should exist");
|
||||||
|
assert_eq!(meta.get("a").unwrap(), &json!(1));
|
||||||
|
assert_eq!(meta.get("b").unwrap(), &json!("two"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_tool_result_without_meta_omits_field() {
|
||||||
|
let result = CallToolResult::success(vec![Content::text("no meta")]);
|
||||||
|
let v = serde_json::to_value(&result).unwrap();
|
||||||
|
// Ensure _meta is omitted
|
||||||
|
assert!(v.get("_meta").is_none());
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue