Replace mcp_core::content types with rmcp::model types (#3500)

This commit is contained in:
Jack Amadeo
2025-07-18 17:23:25 -04:00
committed by GitHub
parent a2309d9436
commit d5291461ca
66 changed files with 867 additions and 856 deletions

View File

@@ -1,313 +0,0 @@
/// Content sent around agents, extensions, and LLMs
/// The various content types can be display to humans but also understood by models
/// They include optional annotations used to help inform agent usage
use super::Role;
use crate::resource::ResourceContents;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Annotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<Role>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = String, format = "date-time", example = "2023-01-01T00:00:00Z")]
// for openapi
pub timestamp: Option<DateTime<Utc>>,
}
impl Annotations {
/// Creates a new Annotations instance specifically for resources
/// optional priority, and a timestamp (defaults to now if None)
pub fn for_resource(priority: f32, timestamp: DateTime<Utc>) -> Self {
assert!(
(0.0..=1.0).contains(&priority),
"Priority {priority} must be between 0.0 and 1.0"
);
Annotations {
priority: Some(priority),
timestamp: Some(timestamp),
audience: None,
}
}
}
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextContent {
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageContent {
pub data: String,
pub mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmbeddedResource {
pub resource: ResourceContents,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
impl EmbeddedResource {
pub fn get_text(&self) -> String {
match &self.resource {
ResourceContents::TextResourceContents { text, .. } => text.clone(),
_ => String::new(),
}
}
}
#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Content {
Text(TextContent),
Image(ImageContent),
Resource(EmbeddedResource),
}
impl Content {
pub fn text<S: Into<String>>(text: S) -> Self {
Content::Text(TextContent {
text: text.into(),
annotations: None,
})
}
pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
Content::Image(ImageContent {
data: data.into(),
mime_type: mime_type.into(),
annotations: None,
})
}
pub fn resource(resource: ResourceContents) -> Self {
Content::Resource(EmbeddedResource {
resource,
annotations: None,
})
}
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
Content::Resource(EmbeddedResource {
resource: ResourceContents::TextResourceContents {
uri: uri.into(),
mime_type: Some("text".to_string()),
text: content.into(),
},
annotations: None,
})
}
/// Get the text content if this is a TextContent variant
pub fn as_text(&self) -> Option<&str> {
match self {
Content::Text(text) => Some(&text.text),
_ => None,
}
}
/// Get the image content if this is an ImageContent variant
pub fn as_image(&self) -> Option<(&str, &str)> {
match self {
Content::Image(image) => Some((&image.data, &image.mime_type)),
_ => None,
}
}
/// Set the audience for the content
pub fn with_audience(mut self, audience: Vec<Role>) -> Self {
let annotations = match &mut self {
Content::Text(text) => &mut text.annotations,
Content::Image(image) => &mut image.annotations,
Content::Resource(resource) => &mut resource.annotations,
};
*annotations = Some(match annotations.take() {
Some(mut a) => {
a.audience = Some(audience);
a
}
None => Annotations {
audience: Some(audience),
priority: None,
timestamp: None,
},
});
self
}
/// Set the priority for the content
/// # Panics
/// Panics if priority is not between 0.0 and 1.0 inclusive
pub fn with_priority(mut self, priority: f32) -> Self {
if !(0.0..=1.0).contains(&priority) {
panic!("Priority must be between 0.0 and 1.0");
}
let annotations = match &mut self {
Content::Text(text) => &mut text.annotations,
Content::Image(image) => &mut image.annotations,
Content::Resource(resource) => &mut resource.annotations,
};
*annotations = Some(match annotations.take() {
Some(mut a) => {
a.priority = Some(priority);
a
}
None => Annotations {
audience: None,
priority: Some(priority),
timestamp: None,
},
});
self
}
/// Get the audience if set
pub fn audience(&self) -> Option<&Vec<Role>> {
match self {
Content::Text(text) => text.annotations.as_ref().and_then(|a| a.audience.as_ref()),
Content::Image(image) => image.annotations.as_ref().and_then(|a| a.audience.as_ref()),
Content::Resource(resource) => resource
.annotations
.as_ref()
.and_then(|a| a.audience.as_ref()),
}
}
/// Get the priority if set
pub fn priority(&self) -> Option<f32> {
match self {
Content::Text(text) => text.annotations.as_ref().and_then(|a| a.priority),
Content::Image(image) => image.annotations.as_ref().and_then(|a| a.priority),
Content::Resource(resource) => resource.annotations.as_ref().and_then(|a| a.priority),
}
}
pub fn unannotated(&self) -> Self {
match self {
Content::Text(text) => Content::text(text.text.clone()),
Content::Image(image) => Content::image(image.data.clone(), image.mime_type.clone()),
Content::Resource(resource) => Content::resource(resource.resource.clone()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_text() {
let content = Content::text("hello");
assert_eq!(content.as_text(), Some("hello"));
assert_eq!(content.as_image(), None);
}
#[test]
fn test_content_image() {
let content = Content::image("data", "image/png");
assert_eq!(content.as_text(), None);
assert_eq!(content.as_image(), Some(("data", "image/png")));
}
#[test]
fn test_content_annotations_basic() {
let content = Content::text("hello")
.with_audience(vec![Role::User])
.with_priority(0.5);
assert_eq!(content.audience(), Some(&vec![Role::User]));
assert_eq!(content.priority(), Some(0.5));
}
#[test]
fn test_content_annotations_order_independence() {
let content1 = Content::text("hello")
.with_audience(vec![Role::User])
.with_priority(0.5);
let content2 = Content::text("hello")
.with_priority(0.5)
.with_audience(vec![Role::User]);
assert_eq!(content1.audience(), content2.audience());
assert_eq!(content1.priority(), content2.priority());
}
#[test]
fn test_content_annotations_overwrite() {
let content = Content::text("hello")
.with_audience(vec![Role::User])
.with_priority(0.5)
.with_audience(vec![Role::Assistant])
.with_priority(0.8);
assert_eq!(content.audience(), Some(&vec![Role::Assistant]));
assert_eq!(content.priority(), Some(0.8));
}
#[test]
fn test_content_annotations_image() {
let content = Content::image("data", "image/png")
.with_audience(vec![Role::User])
.with_priority(0.5);
assert_eq!(content.audience(), Some(&vec![Role::User]));
assert_eq!(content.priority(), Some(0.5));
}
#[test]
fn test_content_annotations_preservation() {
let text_content = Content::text("hello")
.with_audience(vec![Role::User])
.with_priority(0.5);
match &text_content {
Content::Text(TextContent { annotations, .. }) => {
assert!(annotations.is_some());
let ann = annotations.as_ref().unwrap();
assert_eq!(ann.audience, Some(vec![Role::User]));
assert_eq!(ann.priority, Some(0.5));
}
_ => panic!("Expected Text content"),
}
}
#[test]
#[should_panic(expected = "Priority must be between 0.0 and 1.0")]
fn test_invalid_priority() {
Content::text("hello").with_priority(1.5);
}
#[test]
fn test_unannotated() {
let content = Content::text("hello")
.with_audience(vec![Role::User])
.with_priority(0.5);
let unannotated = content.unannotated();
assert_eq!(unannotated.audience(), None);
assert_eq!(unannotated.priority(), None);
}
#[test]
fn test_partial_annotations() {
let content = Content::text("hello").with_priority(0.5);
assert_eq!(content.audience(), None);
assert_eq!(content.priority(), Some(0.5));
let content = Content::text("hello").with_audience(vec![Role::User]);
assert_eq!(content.audience(), Some(&vec![Role::User]));
assert_eq!(content.priority(), None);
}
}

View File

@@ -1,5 +1,3 @@
pub mod content;
pub use content::{Annotations, Content, ImageContent, TextContent};
pub mod handler;
pub mod role;
pub use role::Role;

View File

@@ -1,7 +1,6 @@
use crate::content::{Annotations, EmbeddedResource, ImageContent};
use crate::handler::PromptError;
use crate::resource::ResourceContents;
use base64::engine::{general_purpose::STANDARD as BASE64_STANDARD, Engine};
use rmcp::model::{Annotations, EmbeddedResource, ImageContent};
use serde::{Deserialize, Serialize};
/// A prompt that can be used to generate text from a model
@@ -113,8 +112,7 @@ impl PromptMessage {
role,
content: PromptMessageContent::Image {
image: ImageContent {
data,
mime_type,
raw: rmcp::model::RawImageContent { data, mime_type },
annotations,
},
},
@@ -129,7 +127,7 @@ impl PromptMessage {
text: Option<String>,
annotations: Option<Annotations>,
) -> Self {
let resource_contents = ResourceContents::TextResourceContents {
let resource_contents = rmcp::model::ResourceContents::TextResourceContents {
uri,
mime_type: Some(mime_type),
text: text.unwrap_or_default(),
@@ -139,7 +137,9 @@ impl PromptMessage {
role,
content: PromptMessageContent::Resource {
resource: EmbeddedResource {
resource: resource_contents,
raw: rmcp::model::RawEmbeddedResource {
resource: resource_contents,
},
annotations,
},
},

View File

@@ -1,11 +1,11 @@
/// The protocol messages exchanged between client and server
use crate::{
content::Content,
prompt::{Prompt, PromptMessage},
resource::Resource,
resource::ResourceContents,
tool::Tool,
};
use rmcp::model::Content;
use serde::{Deserialize, Serialize};
use serde_json::Value;

View File

@@ -1,7 +1,7 @@
use crate::content::Annotations;
/// Resources that servers provide to clients
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use rmcp::model::Annotations;
use serde::{Deserialize, Serialize};
use url::Url;
use utoipa::ToSchema;