use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use serde_json::{Map, Value};
use crate::draft::types as draft;
use crate::v2025_06_18::types as v06;
use crate::v2025_11_25::types as legacy;
pub mod result_type {
pub const COMPLETE: &str = "complete";
pub const INPUT_REQUIRED: &str = "input_required";
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Content {
#[non_exhaustive]
Text {
text: String,
annotations: Option<Annotations>,
meta: Map<String, Value>,
},
#[non_exhaustive]
Image {
data: String,
mime_type: String,
annotations: Option<Annotations>,
meta: Map<String, Value>,
},
#[non_exhaustive]
Audio {
data: String,
mime_type: String,
annotations: Option<Annotations>,
meta: Map<String, Value>,
},
#[non_exhaustive]
Resource {
contents: ResourceContents,
annotations: Option<Annotations>,
meta: Map<String, Value>,
},
ResourceLink(Box<Resource>),
}
impl Content {
pub fn text(s: impl Into<String>) -> Self {
Self::Text {
text: s.into(),
annotations: None,
meta: Map::new(),
}
}
pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self::Image {
data: data.into(),
mime_type: mime_type.into(),
annotations: None,
meta: Map::new(),
}
}
pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self::Audio {
data: data.into(),
mime_type: mime_type.into(),
annotations: None,
meta: Map::new(),
}
}
pub fn resource(contents: ResourceContents) -> Self {
Self::Resource {
contents,
annotations: None,
meta: Map::new(),
}
}
pub fn resource_link(resource: Resource) -> Self {
Self::ResourceLink(Box::new(resource))
}
#[must_use]
pub fn with_annotations(mut self, a: Annotations) -> Self {
match &mut self {
Self::Text { annotations, .. }
| Self::Image { annotations, .. }
| Self::Audio { annotations, .. }
| Self::Resource { annotations, .. } => *annotations = Some(a),
Self::ResourceLink(r) => r.annotations = Some(a),
}
self
}
#[must_use]
pub fn with_meta_entry(mut self, key: impl Into<String>, value: Value) -> Self {
match &mut self {
Self::Text { meta, .. }
| Self::Image { meta, .. }
| Self::Audio { meta, .. }
| Self::Resource { meta, .. } => {
meta.insert(key.into(), value);
}
Self::ResourceLink(r) => {
r.meta.insert(key.into(), value);
}
}
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TaskSupport {
Forbidden,
Optional,
Required,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Tool {
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub input_schema: Value,
pub output_schema: Option<Value>,
pub task_support: Option<TaskSupport>,
pub annotations: Option<ToolAnnotations>,
pub icons: Vec<Icon>,
pub meta: Map<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ToolAnnotations {
pub title: Option<String>,
pub read_only_hint: Option<bool>,
pub destructive_hint: Option<bool>,
pub idempotent_hint: Option<bool>,
pub open_world_hint: Option<bool>,
}
impl ToolAnnotations {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn read_only(mut self) -> Self {
self.read_only_hint = Some(true);
self
}
#[must_use]
pub fn destructive(mut self, destructive: bool) -> Self {
self.destructive_hint = Some(destructive);
self
}
#[must_use]
pub fn idempotent(mut self, idempotent: bool) -> Self {
self.idempotent_hint = Some(idempotent);
self
}
#[must_use]
pub fn open_world(mut self, open_world: bool) -> Self {
self.open_world_hint = Some(open_world);
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Icon {
pub src: String,
pub mime_type: Option<String>,
pub sizes: Vec<String>,
pub theme: Option<IconTheme>,
}
impl Icon {
pub fn new(src: impl Into<String>) -> Self {
Self {
src: src.into(),
mime_type: None,
sizes: Vec::new(),
theme: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IconTheme {
Light,
Dark,
}
impl Tool {
pub fn new(name: impl Into<String>, input_schema: Value) -> Self {
Self {
name: name.into(),
title: None,
description: None,
input_schema,
output_schema: None,
task_support: None,
annotations: None,
icons: Vec::new(),
meta: Map::new(),
}
}
#[must_use]
pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
self.annotations = Some(annotations);
self
}
#[must_use]
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icons.push(icon);
self
}
#[must_use]
pub fn with_meta_entry(mut self, key: impl Into<String>, value: Value) -> Self {
self.meta.insert(key.into(), value);
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_output_schema(mut self, output_schema: Value) -> Self {
self.output_schema = Some(output_schema);
self
}
#[must_use]
pub fn with_task_support(mut self, task_support: TaskSupport) -> Self {
self.task_support = Some(task_support);
self
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ListToolsResult {
pub tools: Vec<Tool>,
pub next_cursor: Option<String>,
pub cache: Option<CachePolicy>,
}
impl ListToolsResult {
pub fn new(tools: Vec<Tool>) -> Self {
Self {
tools,
next_cursor: None,
cache: None,
}
}
#[must_use]
pub fn with_cache(mut self, cache: CachePolicy) -> Self {
self.cache = Some(cache);
self
}
}
impl Cacheable for ListToolsResult {
fn cache_policy_mut(&mut self) -> &mut Option<CachePolicy> {
&mut self.cache
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CallToolParams {
pub name: String,
pub arguments: Map<String, Value>,
}
impl CallToolParams {
pub fn new(name: impl Into<String>, arguments: Map<String, Value>) -> Self {
Self {
name: name.into(),
arguments,
}
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CallToolResult {
pub content: Vec<Content>,
pub is_error: bool,
pub structured_content: Option<Value>,
}
impl CallToolResult {
pub fn new(content: Vec<Content>) -> Self {
Self {
content,
is_error: false,
structured_content: None,
}
}
pub fn text(s: impl Into<String>) -> Self {
Self {
content: alloc::vec![Content::text(s)],
is_error: false,
structured_content: None,
}
}
pub fn error(s: impl Into<String>) -> Self {
Self {
content: alloc::vec![Content::text(s)],
is_error: true,
structured_content: None,
}
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ListParams {
pub cursor: Option<String>,
}
impl ListParams {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_cursor(cursor: impl Into<String>) -> Self {
Self {
cursor: Some(cursor.into()),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct SubscriptionFilter {
pub tools_list_changed: bool,
pub resources_list_changed: bool,
pub prompts_list_changed: bool,
pub resource_subscriptions: Vec<String>,
}
impl SubscriptionFilter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn all_list_changed() -> Self {
Self {
tools_list_changed: true,
resources_list_changed: true,
prompts_list_changed: true,
resource_subscriptions: Vec::new(),
}
}
#[must_use]
pub fn with_resource(mut self, uri: impl Into<String>) -> Self {
self.resource_subscriptions.push(uri.into());
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CacheScope {
Private,
Public,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CachePolicy {
pub ttl_ms: u64,
pub scope: CacheScope,
}
impl CachePolicy {
pub const NO_CACHE: CachePolicy = CachePolicy {
ttl_ms: 0,
scope: CacheScope::Private,
};
#[must_use]
pub fn private(ttl: core::time::Duration) -> Self {
Self {
ttl_ms: u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX),
scope: CacheScope::Private,
}
}
#[must_use]
pub fn public(ttl: core::time::Duration) -> Self {
Self {
ttl_ms: u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX),
scope: CacheScope::Public,
}
}
#[must_use]
pub fn from_wire(ttl_ms: u64, scope: CacheScope) -> Self {
Self { ttl_ms, scope }
}
}
pub trait Cacheable {
fn cache_policy_mut(&mut self) -> &mut Option<CachePolicy>;
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Resource {
pub uri: String,
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub mime_type: Option<String>,
pub size: Option<u64>,
pub annotations: Option<Annotations>,
pub icons: Vec<Icon>,
pub meta: Map<String, Value>,
}
impl Resource {
pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
Self {
uri: uri.into(),
name: name.into(),
title: None,
description: None,
mime_type: None,
size: None,
annotations: None,
icons: Vec::new(),
meta: Map::new(),
}
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
#[must_use]
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
#[must_use]
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icons.push(icon);
self
}
#[must_use]
pub fn with_meta_entry(mut self, key: impl Into<String>, value: Value) -> Self {
self.meta.insert(key.into(), value);
self
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Annotations {
pub audience: Vec<Role>,
pub priority: Option<f64>,
pub last_modified: Option<String>,
}
impl Annotations {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn for_audience(mut self, role: Role) -> Self {
self.audience.push(role);
self
}
#[must_use]
pub fn priority(mut self, priority: f64) -> Self {
self.priority = Some(priority);
self
}
#[must_use]
pub fn last_modified(mut self, when: impl Into<String>) -> Self {
self.last_modified = Some(when.into());
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum ResourceContents {
#[non_exhaustive]
Text {
uri: String,
mime_type: Option<String>,
text: String,
meta: Map<String, Value>,
},
#[non_exhaustive]
Blob {
uri: String,
mime_type: Option<String>,
blob: String,
meta: Map<String, Value>,
},
}
impl ResourceContents {
pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
Self::Text {
uri: uri.into(),
mime_type: None,
text: text.into(),
meta: Map::new(),
}
}
pub fn blob(uri: impl Into<String>, blob: impl Into<String>) -> Self {
Self::Blob {
uri: uri.into(),
mime_type: None,
blob: blob.into(),
meta: Map::new(),
}
}
#[must_use]
pub fn with_mime_type(mut self, mime: impl Into<String>) -> Self {
match &mut self {
Self::Text { mime_type, .. } | Self::Blob { mime_type, .. } => {
*mime_type = Some(mime.into());
}
}
self
}
#[must_use]
pub fn with_meta_entry(mut self, key: impl Into<String>, value: Value) -> Self {
match &mut self {
Self::Text { meta, .. } | Self::Blob { meta, .. } => {
meta.insert(key.into(), value);
}
}
self
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ListResourcesResult {
pub resources: Vec<Resource>,
pub next_cursor: Option<String>,
pub cache: Option<CachePolicy>,
}
impl ListResourcesResult {
pub fn new(resources: Vec<Resource>) -> Self {
Self {
resources,
next_cursor: None,
cache: None,
}
}
#[must_use]
pub fn with_cache(mut self, cache: CachePolicy) -> Self {
self.cache = Some(cache);
self
}
}
impl Cacheable for ListResourcesResult {
fn cache_policy_mut(&mut self) -> &mut Option<CachePolicy> {
&mut self.cache
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ReadResourceResult {
pub contents: Vec<ResourceContents>,
pub cache: Option<CachePolicy>,
}
impl ReadResourceResult {
pub fn new(contents: Vec<ResourceContents>) -> Self {
Self {
contents,
cache: None,
}
}
pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
Self {
contents: alloc::vec![ResourceContents::text(uri, text)],
cache: None,
}
}
#[must_use]
pub fn with_cache(mut self, cache: CachePolicy) -> Self {
self.cache = Some(cache);
self
}
}
impl Cacheable for ReadResourceResult {
fn cache_policy_mut(&mut self) -> &mut Option<CachePolicy> {
&mut self.cache
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ResourceTemplate {
pub uri_template: String,
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub mime_type: Option<String>,
pub annotations: Option<Annotations>,
pub icons: Vec<Icon>,
pub meta: Map<String, Value>,
}
impl ResourceTemplate {
pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
Self {
uri_template: uri_template.into(),
name: name.into(),
title: None,
description: None,
mime_type: None,
annotations: None,
icons: Vec::new(),
meta: Map::new(),
}
}
#[must_use]
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
#[must_use]
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icons.push(icon);
self
}
#[must_use]
pub fn with_meta_entry(mut self, key: impl Into<String>, value: Value) -> Self {
self.meta.insert(key.into(), value);
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ListResourceTemplatesResult {
pub resource_templates: Vec<ResourceTemplate>,
pub next_cursor: Option<String>,
pub cache: Option<CachePolicy>,
}
impl ListResourceTemplatesResult {
pub fn new(resource_templates: Vec<ResourceTemplate>) -> Self {
Self {
resource_templates,
next_cursor: None,
cache: None,
}
}
#[must_use]
pub fn with_cache(mut self, cache: CachePolicy) -> Self {
self.cache = Some(cache);
self
}
}
impl Cacheable for ListResourceTemplatesResult {
fn cache_policy_mut(&mut self) -> &mut Option<CachePolicy> {
&mut self.cache
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ReadResourceParams {
pub uri: String,
}
impl ReadResourceParams {
pub fn new(uri: impl Into<String>) -> Self {
Self { uri: uri.into() }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Role {
User,
Assistant,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct PromptArgument {
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub required: bool,
}
impl PromptArgument {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
title: None,
description: None,
required: false,
}
}
#[must_use]
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Prompt {
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub arguments: Vec<PromptArgument>,
pub icons: Vec<Icon>,
pub meta: Map<String, Value>,
}
impl Prompt {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
title: None,
description: None,
arguments: Vec::new(),
icons: Vec::new(),
meta: Map::new(),
}
}
#[must_use]
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icons.push(icon);
self
}
#[must_use]
pub fn with_meta_entry(mut self, key: impl Into<String>, value: Value) -> Self {
self.meta.insert(key.into(), value);
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_argument(mut self, argument: PromptArgument) -> Self {
self.arguments.push(argument);
self
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct PromptMessage {
pub role: Role,
pub content: Content,
}
impl PromptMessage {
pub fn user(content: Content) -> Self {
Self {
role: Role::User,
content,
}
}
pub fn assistant(content: Content) -> Self {
Self {
role: Role::Assistant,
content,
}
}
pub fn user_text(text: impl Into<String>) -> Self {
Self::user(Content::text(text))
}
pub fn assistant_text(text: impl Into<String>) -> Self {
Self::assistant(Content::text(text))
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ListPromptsResult {
pub prompts: Vec<Prompt>,
pub next_cursor: Option<String>,
pub cache: Option<CachePolicy>,
}
impl ListPromptsResult {
pub fn new(prompts: Vec<Prompt>) -> Self {
Self {
prompts,
next_cursor: None,
cache: None,
}
}
#[must_use]
pub fn with_cache(mut self, cache: CachePolicy) -> Self {
self.cache = Some(cache);
self
}
}
impl Cacheable for ListPromptsResult {
fn cache_policy_mut(&mut self) -> &mut Option<CachePolicy> {
&mut self.cache
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct GetPromptResult {
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
}
impl GetPromptResult {
pub fn new(messages: Vec<PromptMessage>) -> Self {
Self {
description: None,
messages,
}
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct GetPromptParams {
pub name: String,
pub arguments: BTreeMap<String, String>,
}
impl GetPromptParams {
pub fn new(name: impl Into<String>, arguments: BTreeMap<String, String>) -> Self {
Self {
name: name.into(),
arguments,
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ElicitParams {
pub message: String,
pub requested_schema: Value,
}
impl ElicitParams {
pub fn new(message: impl Into<String>, requested_schema: Value) -> Self {
Self {
message: message.into(),
requested_schema,
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ElicitUrlParams {
pub message: String,
pub elicitation_id: Option<String>,
pub url: String,
}
impl ElicitUrlParams {
pub fn new(message: impl Into<String>, url: impl Into<String>) -> Self {
Self {
message: message.into(),
elicitation_id: None,
url: url.into(),
}
}
#[must_use]
pub fn with_elicitation_id(mut self, id: impl Into<String>) -> Self {
self.elicitation_id = Some(id.into());
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ElicitAction {
Accept,
Decline,
Cancel,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ElicitOutcome {
pub action: ElicitAction,
pub content: Map<String, Value>,
}
impl ElicitOutcome {
#[must_use]
pub fn new(action: ElicitAction, content: Map<String, Value>) -> Self {
Self { action, content }
}
#[must_use]
pub fn accepted(&self) -> bool {
self.action == ElicitAction::Accept
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CompleteResult {
pub values: Vec<String>,
pub total: Option<u32>,
pub has_more: Option<bool>,
}
impl CompleteResult {
pub fn new(values: Vec<String>) -> Self {
Self {
values,
total: None,
has_more: None,
}
}
#[must_use]
pub fn with_total(mut self, total: u32) -> Self {
self.total = Some(total);
self
}
#[must_use]
pub fn with_has_more(mut self, has_more: bool) -> Self {
self.has_more = Some(has_more);
self
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum CompletionReference {
Prompt {
name: String,
},
ResourceTemplate {
uri: String,
},
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CompletionArgument {
pub name: String,
pub value: String,
}
impl CompletionArgument {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CompleteParams {
pub reference: CompletionReference,
pub argument: CompletionArgument,
pub context_arguments: BTreeMap<String, String>,
}
impl CompleteParams {
pub fn new(reference: CompletionReference, argument: CompletionArgument) -> Self {
Self {
reference,
argument,
context_arguments: BTreeMap::new(),
}
}
}
impl From<Content> for draft::ContentBlock {
fn from(c: Content) -> Self {
match c {
Content::Text {
text,
annotations,
meta,
} => draft::ContentBlock::TextContent(draft::TextContent {
annotations: annotations.map(Into::into),
meta: (!meta.is_empty()).then_some(draft::MetaObject(meta)),
text,
type_: "text".to_string(),
}),
Content::Image {
data,
mime_type,
annotations,
meta,
} => draft::ContentBlock::ImageContent(draft::ImageContent {
annotations: annotations.map(Into::into),
data,
meta: (!meta.is_empty()).then_some(draft::MetaObject(meta)),
mime_type,
type_: "image".to_string(),
}),
Content::Audio {
data,
mime_type,
annotations,
meta,
} => draft::ContentBlock::AudioContent(draft::AudioContent {
annotations: annotations.map(Into::into),
data,
meta: (!meta.is_empty()).then_some(draft::MetaObject(meta)),
mime_type,
type_: "audio".to_string(),
}),
Content::Resource {
contents,
annotations,
meta,
} => draft::ContentBlock::EmbeddedResource(draft::EmbeddedResource {
annotations: annotations.map(Into::into),
meta: (!meta.is_empty()).then_some(draft::MetaObject(meta)),
resource: contents.into(),
type_: "resource".to_string(),
}),
Content::ResourceLink(r) => {
let r = *r;
draft::ContentBlock::ResourceLink(draft::ResourceLink {
annotations: r.annotations.map(Into::into),
description: r.description,
icons: r.icons.into_iter().map(Into::into).collect(),
meta: (!r.meta.is_empty()).then_some(draft::MetaObject(r.meta)),
mime_type: r.mime_type,
name: r.name,
size: r.size.map(|s| i64::try_from(s).unwrap_or(i64::MAX)),
title: r.title,
type_: "resource_link".to_string(),
uri: r.uri,
})
}
}
}
}
impl From<Tool> for draft::Tool {
fn from(t: Tool) -> Self {
let input_schema =
serde_json::from_value(t.input_schema).unwrap_or(draft::ToolInputSchema {
schema: None,
type_: "object".to_string(),
extra: Map::new(),
});
draft::Tool {
annotations: t.annotations.map(Into::into),
description: t.description,
icons: t.icons.into_iter().map(Into::into).collect(),
input_schema,
meta: (!t.meta.is_empty()).then_some(draft::MetaObject(t.meta)),
name: t.name,
output_schema: t.output_schema.and_then(|v| serde_json::from_value(v).ok()),
title: t.title,
}
}
}
impl From<ToolAnnotations> for draft::ToolAnnotations {
fn from(a: ToolAnnotations) -> Self {
draft::ToolAnnotations {
destructive_hint: a.destructive_hint,
idempotent_hint: a.idempotent_hint,
open_world_hint: a.open_world_hint,
read_only_hint: a.read_only_hint,
title: a.title,
}
}
}
impl From<Icon> for draft::Icon {
fn from(i: Icon) -> Self {
draft::Icon {
mime_type: i.mime_type,
sizes: i.sizes,
src: i.src,
theme: i.theme.map(|t| match t {
IconTheme::Light => draft::IconTheme::Light,
IconTheme::Dark => draft::IconTheme::Dark,
}),
}
}
}
impl From<SubscriptionFilter> for draft::SubscriptionFilter {
fn from(f: SubscriptionFilter) -> Self {
draft::SubscriptionFilter {
tools_list_changed: f.tools_list_changed.then_some(true),
resources_list_changed: f.resources_list_changed.then_some(true),
prompts_list_changed: f.prompts_list_changed.then_some(true),
resource_subscriptions: f.resource_subscriptions,
}
}
}
impl From<draft::SubscriptionFilter> for SubscriptionFilter {
fn from(f: draft::SubscriptionFilter) -> Self {
SubscriptionFilter {
tools_list_changed: f.tools_list_changed.unwrap_or(false),
resources_list_changed: f.resources_list_changed.unwrap_or(false),
prompts_list_changed: f.prompts_list_changed.unwrap_or(false),
resource_subscriptions: f.resource_subscriptions,
}
}
}
impl From<ListToolsResult> for draft::ListToolsResult {
fn from(r: ListToolsResult) -> Self {
let cache = r.cache.unwrap_or(CachePolicy::NO_CACHE);
draft::ListToolsResult {
cache_scope: match cache.scope {
CacheScope::Public => draft::ListToolsResultCacheScope::Public,
CacheScope::Private => draft::ListToolsResultCacheScope::Private,
},
meta: None,
next_cursor: r.next_cursor,
result_type: result_type::COMPLETE.to_string(),
tools: r.tools.into_iter().map(Into::into).collect(),
ttl_ms: cache.ttl_ms,
}
}
}
impl From<CallToolResult> for draft::CallToolResult {
fn from(r: CallToolResult) -> Self {
draft::CallToolResult {
content: r.content.into_iter().map(Into::into).collect(),
is_error: Some(r.is_error),
meta: None,
result_type: result_type::COMPLETE.to_string(),
structured_content: r.structured_content,
}
}
}
impl From<Resource> for draft::Resource {
fn from(r: Resource) -> Self {
draft::Resource {
annotations: r.annotations.map(Into::into),
description: r.description,
icons: r.icons.into_iter().map(Into::into).collect(),
meta: (!r.meta.is_empty()).then_some(draft::MetaObject(r.meta)),
mime_type: r.mime_type,
name: r.name,
size: r.size.map(|s| i64::try_from(s).unwrap_or(i64::MAX)),
title: r.title,
uri: r.uri,
}
}
}
impl From<Annotations> for draft::Annotations {
fn from(a: Annotations) -> Self {
draft::Annotations {
audience: a.audience.into_iter().map(Into::into).collect(),
last_modified: a.last_modified,
priority: a.priority,
}
}
}
impl From<ResourceContents> for draft::ReadResourceResultContentsItem {
fn from(c: ResourceContents) -> Self {
match c {
ResourceContents::Text {
uri,
mime_type,
text,
meta,
} => draft::ReadResourceResultContentsItem::TextResourceContents(
draft::TextResourceContents {
meta: (!meta.is_empty()).then_some(draft::MetaObject(meta)),
mime_type,
text,
uri,
},
),
ResourceContents::Blob {
uri,
mime_type,
blob,
meta,
} => draft::ReadResourceResultContentsItem::BlobResourceContents(
draft::BlobResourceContents {
blob,
meta: (!meta.is_empty()).then_some(draft::MetaObject(meta)),
mime_type,
uri,
},
),
}
}
}
impl From<ResourceContents> for draft::EmbeddedResourceResource {
fn from(c: ResourceContents) -> Self {
match c {
ResourceContents::Text {
uri,
mime_type,
text,
meta,
} => {
draft::EmbeddedResourceResource::TextResourceContents(draft::TextResourceContents {
meta: (!meta.is_empty()).then_some(draft::MetaObject(meta)),
mime_type,
text,
uri,
})
}
ResourceContents::Blob {
uri,
mime_type,
blob,
meta,
} => {
draft::EmbeddedResourceResource::BlobResourceContents(draft::BlobResourceContents {
blob,
meta: (!meta.is_empty()).then_some(draft::MetaObject(meta)),
mime_type,
uri,
})
}
}
}
}
impl From<ListResourcesResult> for draft::ListResourcesResult {
fn from(r: ListResourcesResult) -> Self {
let cache = r.cache.unwrap_or(CachePolicy::NO_CACHE);
draft::ListResourcesResult {
cache_scope: match cache.scope {
CacheScope::Public => draft::ListResourcesResultCacheScope::Public,
CacheScope::Private => draft::ListResourcesResultCacheScope::Private,
},
meta: None,
next_cursor: r.next_cursor,
resources: r.resources.into_iter().map(Into::into).collect(),
result_type: result_type::COMPLETE.to_string(),
ttl_ms: cache.ttl_ms,
}
}
}
impl From<ReadResourceResult> for draft::ReadResourceResult {
fn from(r: ReadResourceResult) -> Self {
let cache = r.cache.unwrap_or(CachePolicy::NO_CACHE);
draft::ReadResourceResult {
cache_scope: match cache.scope {
CacheScope::Public => draft::ReadResourceResultCacheScope::Public,
CacheScope::Private => draft::ReadResourceResultCacheScope::Private,
},
contents: r.contents.into_iter().map(Into::into).collect(),
meta: None,
result_type: result_type::COMPLETE.to_string(),
ttl_ms: cache.ttl_ms,
}
}
}
impl From<ResourceTemplate> for draft::ResourceTemplate {
fn from(t: ResourceTemplate) -> Self {
draft::ResourceTemplate {
annotations: t.annotations.map(Into::into),
description: t.description,
icons: t.icons.into_iter().map(Into::into).collect(),
meta: (!t.meta.is_empty()).then_some(draft::MetaObject(t.meta)),
mime_type: t.mime_type,
name: t.name,
title: t.title,
uri_template: t.uri_template,
}
}
}
impl From<ListResourceTemplatesResult> for draft::ListResourceTemplatesResult {
fn from(r: ListResourceTemplatesResult) -> Self {
let cache = r.cache.unwrap_or(CachePolicy::NO_CACHE);
draft::ListResourceTemplatesResult {
cache_scope: match cache.scope {
CacheScope::Public => draft::ListResourceTemplatesResultCacheScope::Public,
CacheScope::Private => draft::ListResourceTemplatesResultCacheScope::Private,
},
meta: None,
next_cursor: r.next_cursor,
resource_templates: r.resource_templates.into_iter().map(Into::into).collect(),
result_type: result_type::COMPLETE.to_string(),
ttl_ms: cache.ttl_ms,
}
}
}
impl From<Role> for draft::Role {
fn from(r: Role) -> Self {
match r {
Role::User => draft::Role::User,
Role::Assistant => draft::Role::Assistant,
}
}
}
impl From<PromptArgument> for draft::PromptArgument {
fn from(a: PromptArgument) -> Self {
draft::PromptArgument {
description: a.description,
name: a.name,
required: Some(a.required),
title: a.title,
}
}
}
impl From<Prompt> for draft::Prompt {
fn from(p: Prompt) -> Self {
draft::Prompt {
arguments: p.arguments.into_iter().map(Into::into).collect(),
description: p.description,
icons: p.icons.into_iter().map(Into::into).collect(),
meta: (!p.meta.is_empty()).then_some(draft::MetaObject(p.meta)),
name: p.name,
title: p.title,
}
}
}
impl From<PromptMessage> for draft::PromptMessage {
fn from(m: PromptMessage) -> Self {
draft::PromptMessage {
content: m.content.into(),
role: m.role.into(),
}
}
}
impl From<ListPromptsResult> for draft::ListPromptsResult {
fn from(r: ListPromptsResult) -> Self {
let cache = r.cache.unwrap_or(CachePolicy::NO_CACHE);
draft::ListPromptsResult {
cache_scope: match cache.scope {
CacheScope::Public => draft::ListPromptsResultCacheScope::Public,
CacheScope::Private => draft::ListPromptsResultCacheScope::Private,
},
meta: None,
next_cursor: r.next_cursor,
prompts: r.prompts.into_iter().map(Into::into).collect(),
result_type: result_type::COMPLETE.to_string(),
ttl_ms: cache.ttl_ms,
}
}
}
impl From<GetPromptResult> for draft::GetPromptResult {
fn from(r: GetPromptResult) -> Self {
draft::GetPromptResult {
description: r.description,
messages: r.messages.into_iter().map(Into::into).collect(),
meta: None,
result_type: result_type::COMPLETE.to_string(),
}
}
}
impl From<CompleteResult> for draft::CompleteResult {
fn from(r: CompleteResult) -> Self {
draft::CompleteResult {
completion: draft::CompleteResultCompletion {
has_more: r.has_more,
total: r.total.map(i64::from),
values: r.values,
},
meta: None,
result_type: result_type::COMPLETE.to_string(),
}
}
}
impl From<Content> for legacy::ContentBlock {
fn from(c: Content) -> Self {
match c {
Content::Text {
text,
annotations,
meta,
} => legacy::ContentBlock::TextContent(legacy::TextContent {
annotations: annotations.map(Into::into),
meta,
text,
type_: "text".to_string(),
}),
Content::Image {
data,
mime_type,
annotations,
meta,
} => legacy::ContentBlock::ImageContent(legacy::ImageContent {
annotations: annotations.map(Into::into),
data,
meta,
mime_type,
type_: "image".to_string(),
}),
Content::Audio {
data,
mime_type,
annotations,
meta,
} => legacy::ContentBlock::AudioContent(legacy::AudioContent {
annotations: annotations.map(Into::into),
data,
meta,
mime_type,
type_: "audio".to_string(),
}),
Content::Resource {
contents,
annotations,
meta,
} => legacy::ContentBlock::EmbeddedResource(legacy::EmbeddedResource {
annotations: annotations.map(Into::into),
meta,
resource: contents.into(),
type_: "resource".to_string(),
}),
Content::ResourceLink(r) => {
let r = *r;
legacy::ContentBlock::ResourceLink(legacy::ResourceLink {
annotations: r.annotations.map(Into::into),
description: r.description,
icons: r.icons.into_iter().map(Into::into).collect(),
meta: r.meta,
mime_type: r.mime_type,
name: r.name,
size: r.size.map(|s| i64::try_from(s).unwrap_or(i64::MAX)),
title: r.title,
type_: "resource_link".to_string(),
uri: r.uri,
})
}
}
}
}
impl From<TaskSupport> for legacy::ToolExecutionTaskSupport {
fn from(ts: TaskSupport) -> Self {
match ts {
TaskSupport::Forbidden => legacy::ToolExecutionTaskSupport::Forbidden,
TaskSupport::Optional => legacy::ToolExecutionTaskSupport::Optional,
TaskSupport::Required => legacy::ToolExecutionTaskSupport::Required,
}
}
}
impl From<legacy::ToolExecutionTaskSupport> for TaskSupport {
fn from(ts: legacy::ToolExecutionTaskSupport) -> Self {
match ts {
legacy::ToolExecutionTaskSupport::Forbidden => TaskSupport::Forbidden,
legacy::ToolExecutionTaskSupport::Optional => TaskSupport::Optional,
legacy::ToolExecutionTaskSupport::Required => TaskSupport::Required,
}
}
}
impl From<Tool> for legacy::Tool {
fn from(t: Tool) -> Self {
let input_schema =
serde_json::from_value(t.input_schema).unwrap_or(legacy::ToolInputSchema {
properties: BTreeMap::new(),
required: Vec::new(),
schema: None,
type_: "object".to_string(),
extra: Map::new(),
});
legacy::Tool {
annotations: t.annotations.map(Into::into),
description: t.description,
execution: t.task_support.map(|ts| legacy::ToolExecution {
task_support: Some(ts.into()),
}),
icons: t.icons.into_iter().map(Into::into).collect(),
input_schema,
meta: t.meta,
name: t.name,
output_schema: t.output_schema.and_then(|v| serde_json::from_value(v).ok()),
title: t.title,
}
}
}
impl From<ToolAnnotations> for legacy::ToolAnnotations {
fn from(a: ToolAnnotations) -> Self {
legacy::ToolAnnotations {
destructive_hint: a.destructive_hint,
idempotent_hint: a.idempotent_hint,
open_world_hint: a.open_world_hint,
read_only_hint: a.read_only_hint,
title: a.title,
}
}
}
impl From<Icon> for legacy::Icon {
fn from(i: Icon) -> Self {
legacy::Icon {
mime_type: i.mime_type,
sizes: i.sizes,
src: i.src,
theme: i.theme.map(|t| match t {
IconTheme::Light => legacy::IconTheme::Light,
IconTheme::Dark => legacy::IconTheme::Dark,
}),
}
}
}
impl From<ListToolsResult> for legacy::ListToolsResult {
fn from(r: ListToolsResult) -> Self {
legacy::ListToolsResult {
meta: Map::new(),
next_cursor: r.next_cursor,
tools: r.tools.into_iter().map(Into::into).collect(),
}
}
}
impl From<CallToolResult> for legacy::CallToolResult {
fn from(r: CallToolResult) -> Self {
let structured_content = match r.structured_content {
Some(Value::Object(map)) => map,
_ => Map::new(),
};
legacy::CallToolResult {
content: r.content.into_iter().map(Into::into).collect(),
is_error: Some(r.is_error),
meta: Map::new(),
structured_content,
}
}
}
impl From<Resource> for legacy::Resource {
fn from(r: Resource) -> Self {
legacy::Resource {
annotations: r.annotations.map(Into::into),
description: r.description,
icons: r.icons.into_iter().map(Into::into).collect(),
meta: r.meta,
mime_type: r.mime_type,
name: r.name,
size: r.size.map(|s| i64::try_from(s).unwrap_or(i64::MAX)),
title: r.title,
uri: r.uri,
}
}
}
impl From<Annotations> for legacy::Annotations {
fn from(a: Annotations) -> Self {
legacy::Annotations {
audience: a.audience.into_iter().map(Into::into).collect(),
last_modified: a.last_modified,
priority: a.priority,
}
}
}
impl From<ResourceContents> for legacy::ReadResourceResultContentsItem {
fn from(c: ResourceContents) -> Self {
match c {
ResourceContents::Text {
uri,
mime_type,
text,
meta,
} => legacy::ReadResourceResultContentsItem::TextResourceContents(
legacy::TextResourceContents {
meta,
mime_type,
text,
uri,
},
),
ResourceContents::Blob {
uri,
mime_type,
blob,
meta,
} => legacy::ReadResourceResultContentsItem::BlobResourceContents(
legacy::BlobResourceContents {
blob,
meta,
mime_type,
uri,
},
),
}
}
}
impl From<ResourceContents> for legacy::EmbeddedResourceResource {
fn from(c: ResourceContents) -> Self {
match c {
ResourceContents::Text {
uri,
mime_type,
text,
meta,
} => legacy::EmbeddedResourceResource::TextResourceContents(
legacy::TextResourceContents {
meta,
mime_type,
text,
uri,
},
),
ResourceContents::Blob {
uri,
mime_type,
blob,
meta,
} => legacy::EmbeddedResourceResource::BlobResourceContents(
legacy::BlobResourceContents {
blob,
meta,
mime_type,
uri,
},
),
}
}
}
impl From<ListResourcesResult> for legacy::ListResourcesResult {
fn from(r: ListResourcesResult) -> Self {
legacy::ListResourcesResult {
meta: Map::new(),
next_cursor: r.next_cursor,
resources: r.resources.into_iter().map(Into::into).collect(),
}
}
}
impl From<ReadResourceResult> for legacy::ReadResourceResult {
fn from(r: ReadResourceResult) -> Self {
legacy::ReadResourceResult {
contents: r.contents.into_iter().map(Into::into).collect(),
meta: Map::new(),
}
}
}
impl From<ResourceTemplate> for legacy::ResourceTemplate {
fn from(t: ResourceTemplate) -> Self {
legacy::ResourceTemplate {
annotations: t.annotations.map(Into::into),
description: t.description,
icons: t.icons.into_iter().map(Into::into).collect(),
meta: t.meta,
mime_type: t.mime_type,
name: t.name,
title: t.title,
uri_template: t.uri_template,
}
}
}
impl From<ListResourceTemplatesResult> for legacy::ListResourceTemplatesResult {
fn from(r: ListResourceTemplatesResult) -> Self {
legacy::ListResourceTemplatesResult {
meta: Map::new(),
next_cursor: r.next_cursor,
resource_templates: r.resource_templates.into_iter().map(Into::into).collect(),
}
}
}
impl From<Role> for legacy::Role {
fn from(r: Role) -> Self {
match r {
Role::User => legacy::Role::User,
Role::Assistant => legacy::Role::Assistant,
}
}
}
impl From<PromptArgument> for legacy::PromptArgument {
fn from(a: PromptArgument) -> Self {
legacy::PromptArgument {
description: a.description,
name: a.name,
required: Some(a.required),
title: a.title,
}
}
}
impl From<Prompt> for legacy::Prompt {
fn from(p: Prompt) -> Self {
legacy::Prompt {
arguments: p.arguments.into_iter().map(Into::into).collect(),
description: p.description,
icons: p.icons.into_iter().map(Into::into).collect(),
meta: p.meta,
name: p.name,
title: p.title,
}
}
}
impl From<PromptMessage> for legacy::PromptMessage {
fn from(m: PromptMessage) -> Self {
legacy::PromptMessage {
content: m.content.into(),
role: m.role.into(),
}
}
}
impl From<ListPromptsResult> for legacy::ListPromptsResult {
fn from(r: ListPromptsResult) -> Self {
legacy::ListPromptsResult {
meta: Map::new(),
next_cursor: r.next_cursor,
prompts: r.prompts.into_iter().map(Into::into).collect(),
}
}
}
impl From<GetPromptResult> for legacy::GetPromptResult {
fn from(r: GetPromptResult) -> Self {
legacy::GetPromptResult {
description: r.description,
messages: r.messages.into_iter().map(Into::into).collect(),
meta: Map::new(),
}
}
}
impl From<CompleteResult> for legacy::CompleteResult {
fn from(r: CompleteResult) -> Self {
legacy::CompleteResult {
completion: legacy::CompleteResultCompletion {
has_more: r.has_more,
total: r.total.map(i64::from),
values: r.values,
},
meta: Map::new(),
}
}
}
impl From<draft::ContentBlock> for Content {
fn from(c: draft::ContentBlock) -> Self {
match c {
draft::ContentBlock::TextContent(t) => Content::Text {
text: t.text,
annotations: t.annotations.map(Into::into),
meta: t.meta.map(|m| m.0).unwrap_or_default(),
},
draft::ContentBlock::ImageContent(i) => Content::Image {
data: i.data,
mime_type: i.mime_type,
annotations: i.annotations.map(Into::into),
meta: i.meta.map(|m| m.0).unwrap_or_default(),
},
draft::ContentBlock::AudioContent(a) => Content::Audio {
data: a.data,
mime_type: a.mime_type,
annotations: a.annotations.map(Into::into),
meta: a.meta.map(|m| m.0).unwrap_or_default(),
},
draft::ContentBlock::EmbeddedResource(e) => Content::Resource {
contents: e.resource.into(),
annotations: e.annotations.map(Into::into),
meta: e.meta.map(|m| m.0).unwrap_or_default(),
},
draft::ContentBlock::ResourceLink(l) => Content::ResourceLink(Box::new(l.into())),
}
}
}
impl From<draft::Tool> for Tool {
fn from(t: draft::Tool) -> Self {
Tool {
name: t.name,
title: t.title,
description: t.description,
input_schema: serde_json::to_value(&t.input_schema)
.unwrap_or_else(|_| Value::Object(Map::new())),
output_schema: t.output_schema.and_then(|s| serde_json::to_value(s).ok()),
task_support: None,
annotations: t.annotations.map(Into::into),
icons: t.icons.into_iter().map(Into::into).collect(),
meta: t.meta.map(|m| m.0).unwrap_or_default(),
}
}
}
impl From<draft::ToolAnnotations> for ToolAnnotations {
fn from(a: draft::ToolAnnotations) -> Self {
ToolAnnotations {
title: a.title,
read_only_hint: a.read_only_hint,
destructive_hint: a.destructive_hint,
idempotent_hint: a.idempotent_hint,
open_world_hint: a.open_world_hint,
}
}
}
impl From<draft::Icon> for Icon {
fn from(i: draft::Icon) -> Self {
Icon {
src: i.src,
mime_type: i.mime_type,
sizes: i.sizes,
theme: i.theme.map(|t| match t {
draft::IconTheme::Light => IconTheme::Light,
draft::IconTheme::Dark => IconTheme::Dark,
}),
}
}
}
impl From<draft::ListToolsResult> for ListToolsResult {
fn from(r: draft::ListToolsResult) -> Self {
ListToolsResult {
tools: r.tools.into_iter().map(Into::into).collect(),
next_cursor: r.next_cursor,
cache: Some(CachePolicy::from_wire(
r.ttl_ms,
match r.cache_scope {
draft::ListToolsResultCacheScope::Public => CacheScope::Public,
draft::ListToolsResultCacheScope::Private => CacheScope::Private,
},
)),
}
}
}
impl From<draft::CallToolResult> for CallToolResult {
fn from(r: draft::CallToolResult) -> Self {
CallToolResult {
content: r.content.into_iter().map(Into::into).collect(),
is_error: r.is_error.unwrap_or(false),
structured_content: r.structured_content,
}
}
}
impl From<draft::Resource> for Resource {
fn from(r: draft::Resource) -> Self {
Resource {
uri: r.uri,
name: r.name,
title: r.title,
description: r.description,
mime_type: r.mime_type,
size: r.size.map(|s| u64::try_from(s).unwrap_or(0)),
annotations: r.annotations.map(Into::into),
icons: r.icons.into_iter().map(Into::into).collect(),
meta: r.meta.map(|m| m.0).unwrap_or_default(),
}
}
}
impl From<draft::Annotations> for Annotations {
fn from(a: draft::Annotations) -> Self {
Annotations {
audience: a.audience.into_iter().map(Into::into).collect(),
priority: a.priority,
last_modified: a.last_modified,
}
}
}
impl From<draft::ResourceLink> for Resource {
fn from(l: draft::ResourceLink) -> Self {
Resource {
uri: l.uri,
name: l.name,
title: l.title,
description: l.description,
mime_type: l.mime_type,
size: l.size.map(|s| u64::try_from(s).unwrap_or(0)),
annotations: l.annotations.map(Into::into),
icons: l.icons.into_iter().map(Into::into).collect(),
meta: l.meta.map(|m| m.0).unwrap_or_default(),
}
}
}
impl From<draft::EmbeddedResourceResource> for ResourceContents {
fn from(r: draft::EmbeddedResourceResource) -> Self {
match r {
draft::EmbeddedResourceResource::TextResourceContents(t) => ResourceContents::Text {
uri: t.uri,
mime_type: t.mime_type,
text: t.text,
meta: t.meta.map(|m| m.0).unwrap_or_default(),
},
draft::EmbeddedResourceResource::BlobResourceContents(b) => ResourceContents::Blob {
uri: b.uri,
mime_type: b.mime_type,
blob: b.blob,
meta: b.meta.map(|m| m.0).unwrap_or_default(),
},
}
}
}
impl From<draft::ReadResourceResultContentsItem> for ResourceContents {
fn from(c: draft::ReadResourceResultContentsItem) -> Self {
match c {
draft::ReadResourceResultContentsItem::TextResourceContents(t) => {
ResourceContents::Text {
uri: t.uri,
mime_type: t.mime_type,
text: t.text,
meta: t.meta.map(|m| m.0).unwrap_or_default(),
}
}
draft::ReadResourceResultContentsItem::BlobResourceContents(b) => {
ResourceContents::Blob {
uri: b.uri,
mime_type: b.mime_type,
blob: b.blob,
meta: b.meta.map(|m| m.0).unwrap_or_default(),
}
}
}
}
}
impl From<draft::ListResourcesResult> for ListResourcesResult {
fn from(r: draft::ListResourcesResult) -> Self {
ListResourcesResult {
resources: r.resources.into_iter().map(Into::into).collect(),
next_cursor: r.next_cursor,
cache: Some(CachePolicy::from_wire(
r.ttl_ms,
match r.cache_scope {
draft::ListResourcesResultCacheScope::Public => CacheScope::Public,
draft::ListResourcesResultCacheScope::Private => CacheScope::Private,
},
)),
}
}
}
impl From<draft::ReadResourceResult> for ReadResourceResult {
fn from(r: draft::ReadResourceResult) -> Self {
ReadResourceResult {
contents: r.contents.into_iter().map(Into::into).collect(),
cache: Some(CachePolicy::from_wire(
r.ttl_ms,
match r.cache_scope {
draft::ReadResourceResultCacheScope::Public => CacheScope::Public,
draft::ReadResourceResultCacheScope::Private => CacheScope::Private,
},
)),
}
}
}
impl From<draft::ResourceTemplate> for ResourceTemplate {
fn from(t: draft::ResourceTemplate) -> Self {
ResourceTemplate {
uri_template: t.uri_template,
name: t.name,
title: t.title,
description: t.description,
mime_type: t.mime_type,
annotations: t.annotations.map(Into::into),
icons: t.icons.into_iter().map(Into::into).collect(),
meta: t.meta.map(|m| m.0).unwrap_or_default(),
}
}
}
impl From<draft::ListResourceTemplatesResult> for ListResourceTemplatesResult {
fn from(r: draft::ListResourceTemplatesResult) -> Self {
ListResourceTemplatesResult {
resource_templates: r.resource_templates.into_iter().map(Into::into).collect(),
next_cursor: r.next_cursor,
cache: Some(CachePolicy::from_wire(
r.ttl_ms,
match r.cache_scope {
draft::ListResourceTemplatesResultCacheScope::Public => CacheScope::Public,
draft::ListResourceTemplatesResultCacheScope::Private => CacheScope::Private,
},
)),
}
}
}
impl From<draft::Role> for Role {
fn from(r: draft::Role) -> Self {
match r {
draft::Role::User => Role::User,
draft::Role::Assistant => Role::Assistant,
}
}
}
impl From<draft::PromptArgument> for PromptArgument {
fn from(a: draft::PromptArgument) -> Self {
PromptArgument {
name: a.name,
title: a.title,
description: a.description,
required: a.required.unwrap_or(false),
}
}
}
impl From<draft::Prompt> for Prompt {
fn from(p: draft::Prompt) -> Self {
Prompt {
name: p.name,
title: p.title,
description: p.description,
arguments: p.arguments.into_iter().map(Into::into).collect(),
icons: p.icons.into_iter().map(Into::into).collect(),
meta: p.meta.map(|m| m.0).unwrap_or_default(),
}
}
}
impl From<draft::PromptMessage> for PromptMessage {
fn from(m: draft::PromptMessage) -> Self {
PromptMessage {
role: m.role.into(),
content: m.content.into(),
}
}
}
impl From<draft::ListPromptsResult> for ListPromptsResult {
fn from(r: draft::ListPromptsResult) -> Self {
ListPromptsResult {
prompts: r.prompts.into_iter().map(Into::into).collect(),
next_cursor: r.next_cursor,
cache: Some(CachePolicy::from_wire(
r.ttl_ms,
match r.cache_scope {
draft::ListPromptsResultCacheScope::Public => CacheScope::Public,
draft::ListPromptsResultCacheScope::Private => CacheScope::Private,
},
)),
}
}
}
impl From<draft::GetPromptResult> for GetPromptResult {
fn from(r: draft::GetPromptResult) -> Self {
GetPromptResult {
description: r.description,
messages: r.messages.into_iter().map(Into::into).collect(),
}
}
}
impl From<draft::CompleteResult> for CompleteResult {
fn from(r: draft::CompleteResult) -> Self {
CompleteResult {
values: r.completion.values,
total: r
.completion
.total
.map(|t| u32::try_from(t).unwrap_or(u32::MAX)),
has_more: r.completion.has_more,
}
}
}
impl From<legacy::ContentBlock> for Content {
fn from(c: legacy::ContentBlock) -> Self {
match c {
legacy::ContentBlock::TextContent(t) => Content::Text {
text: t.text,
annotations: t.annotations.map(Into::into),
meta: t.meta,
},
legacy::ContentBlock::ImageContent(i) => Content::Image {
data: i.data,
mime_type: i.mime_type,
annotations: i.annotations.map(Into::into),
meta: i.meta,
},
legacy::ContentBlock::AudioContent(a) => Content::Audio {
data: a.data,
mime_type: a.mime_type,
annotations: a.annotations.map(Into::into),
meta: a.meta,
},
legacy::ContentBlock::EmbeddedResource(e) => Content::Resource {
contents: e.resource.into(),
annotations: e.annotations.map(Into::into),
meta: e.meta,
},
legacy::ContentBlock::ResourceLink(l) => Content::ResourceLink(Box::new(l.into())),
}
}
}
impl From<legacy::Tool> for Tool {
fn from(t: legacy::Tool) -> Self {
Tool {
name: t.name,
title: t.title,
description: t.description,
input_schema: serde_json::to_value(&t.input_schema)
.unwrap_or_else(|_| Value::Object(Map::new())),
output_schema: t.output_schema.and_then(|s| serde_json::to_value(s).ok()),
task_support: t.execution.and_then(|e| e.task_support).map(Into::into),
annotations: t.annotations.map(Into::into),
icons: t.icons.into_iter().map(Into::into).collect(),
meta: t.meta,
}
}
}
impl From<legacy::ToolAnnotations> for ToolAnnotations {
fn from(a: legacy::ToolAnnotations) -> Self {
ToolAnnotations {
title: a.title,
read_only_hint: a.read_only_hint,
destructive_hint: a.destructive_hint,
idempotent_hint: a.idempotent_hint,
open_world_hint: a.open_world_hint,
}
}
}
impl From<legacy::Icon> for Icon {
fn from(i: legacy::Icon) -> Self {
Icon {
src: i.src,
mime_type: i.mime_type,
sizes: i.sizes,
theme: i.theme.map(|t| match t {
legacy::IconTheme::Light => IconTheme::Light,
legacy::IconTheme::Dark => IconTheme::Dark,
}),
}
}
}
impl From<legacy::ListToolsResult> for ListToolsResult {
fn from(r: legacy::ListToolsResult) -> Self {
ListToolsResult {
tools: r.tools.into_iter().map(Into::into).collect(),
next_cursor: r.next_cursor,
cache: None,
}
}
}
impl From<legacy::CallToolResult> for CallToolResult {
fn from(r: legacy::CallToolResult) -> Self {
CallToolResult {
content: r.content.into_iter().map(Into::into).collect(),
is_error: r.is_error.unwrap_or(false),
structured_content: if r.structured_content.is_empty() {
None
} else {
Some(Value::Object(r.structured_content))
},
}
}
}
impl From<legacy::Resource> for Resource {
fn from(r: legacy::Resource) -> Self {
Resource {
uri: r.uri,
name: r.name,
title: r.title,
description: r.description,
mime_type: r.mime_type,
size: r.size.map(|s| u64::try_from(s).unwrap_or(0)),
annotations: r.annotations.map(Into::into),
icons: r.icons.into_iter().map(Into::into).collect(),
meta: r.meta,
}
}
}
impl From<legacy::Annotations> for Annotations {
fn from(a: legacy::Annotations) -> Self {
Annotations {
audience: a.audience.into_iter().map(Into::into).collect(),
priority: a.priority,
last_modified: a.last_modified,
}
}
}
impl From<legacy::ResourceLink> for Resource {
fn from(l: legacy::ResourceLink) -> Self {
Resource {
uri: l.uri,
name: l.name,
title: l.title,
description: l.description,
mime_type: l.mime_type,
size: l.size.map(|s| u64::try_from(s).unwrap_or(0)),
annotations: l.annotations.map(Into::into),
icons: l.icons.into_iter().map(Into::into).collect(),
meta: l.meta,
}
}
}
impl From<legacy::EmbeddedResourceResource> for ResourceContents {
fn from(r: legacy::EmbeddedResourceResource) -> Self {
match r {
legacy::EmbeddedResourceResource::TextResourceContents(t) => ResourceContents::Text {
uri: t.uri,
mime_type: t.mime_type,
text: t.text,
meta: t.meta,
},
legacy::EmbeddedResourceResource::BlobResourceContents(b) => ResourceContents::Blob {
uri: b.uri,
mime_type: b.mime_type,
blob: b.blob,
meta: b.meta,
},
}
}
}
impl From<legacy::ReadResourceResultContentsItem> for ResourceContents {
fn from(c: legacy::ReadResourceResultContentsItem) -> Self {
match c {
legacy::ReadResourceResultContentsItem::TextResourceContents(t) => {
ResourceContents::Text {
uri: t.uri,
mime_type: t.mime_type,
text: t.text,
meta: t.meta,
}
}
legacy::ReadResourceResultContentsItem::BlobResourceContents(b) => {
ResourceContents::Blob {
uri: b.uri,
mime_type: b.mime_type,
blob: b.blob,
meta: b.meta,
}
}
}
}
}
impl From<legacy::ListResourcesResult> for ListResourcesResult {
fn from(r: legacy::ListResourcesResult) -> Self {
ListResourcesResult {
resources: r.resources.into_iter().map(Into::into).collect(),
next_cursor: r.next_cursor,
cache: None,
}
}
}
impl From<legacy::ReadResourceResult> for ReadResourceResult {
fn from(r: legacy::ReadResourceResult) -> Self {
ReadResourceResult {
contents: r.contents.into_iter().map(Into::into).collect(),
cache: None,
}
}
}
impl From<legacy::ResourceTemplate> for ResourceTemplate {
fn from(t: legacy::ResourceTemplate) -> Self {
ResourceTemplate {
uri_template: t.uri_template,
name: t.name,
title: t.title,
description: t.description,
mime_type: t.mime_type,
annotations: t.annotations.map(Into::into),
icons: t.icons.into_iter().map(Into::into).collect(),
meta: t.meta,
}
}
}
impl From<legacy::ListResourceTemplatesResult> for ListResourceTemplatesResult {
fn from(r: legacy::ListResourceTemplatesResult) -> Self {
ListResourceTemplatesResult {
resource_templates: r.resource_templates.into_iter().map(Into::into).collect(),
next_cursor: r.next_cursor,
cache: None,
}
}
}
impl From<legacy::Role> for Role {
fn from(r: legacy::Role) -> Self {
match r {
legacy::Role::User => Role::User,
legacy::Role::Assistant => Role::Assistant,
}
}
}
impl From<legacy::PromptArgument> for PromptArgument {
fn from(a: legacy::PromptArgument) -> Self {
PromptArgument {
name: a.name,
title: a.title,
description: a.description,
required: a.required.unwrap_or(false),
}
}
}
impl From<legacy::Prompt> for Prompt {
fn from(p: legacy::Prompt) -> Self {
Prompt {
name: p.name,
title: p.title,
description: p.description,
arguments: p.arguments.into_iter().map(Into::into).collect(),
icons: p.icons.into_iter().map(Into::into).collect(),
meta: p.meta,
}
}
}
impl From<legacy::PromptMessage> for PromptMessage {
fn from(m: legacy::PromptMessage) -> Self {
PromptMessage {
role: m.role.into(),
content: m.content.into(),
}
}
}
impl From<legacy::ListPromptsResult> for ListPromptsResult {
fn from(r: legacy::ListPromptsResult) -> Self {
ListPromptsResult {
prompts: r.prompts.into_iter().map(Into::into).collect(),
next_cursor: r.next_cursor,
cache: None,
}
}
}
impl From<legacy::GetPromptResult> for GetPromptResult {
fn from(r: legacy::GetPromptResult) -> Self {
GetPromptResult {
description: r.description,
messages: r.messages.into_iter().map(Into::into).collect(),
}
}
}
impl From<legacy::CompleteResult> for CompleteResult {
fn from(r: legacy::CompleteResult) -> Self {
CompleteResult {
values: r.completion.values,
total: r
.completion
.total
.map(|t| u32::try_from(t).unwrap_or(u32::MAX)),
has_more: r.completion.has_more,
}
}
}
macro_rules! neutral_via_legacy {
($($ty:ident),+ $(,)?) => {$(
impl From<$ty> for v06::$ty {
fn from(n: $ty) -> Self {
legacy::$ty::from(n).into()
}
}
impl From<v06::$ty> for $ty {
fn from(w: v06::$ty) -> Self {
legacy::$ty::from(w).into()
}
}
)+};
}
neutral_via_legacy!(
ListToolsResult,
CallToolResult,
ListResourcesResult,
ListResourceTemplatesResult,
ReadResourceResult,
ListPromptsResult,
GetPromptResult,
CompleteResult,
Tool,
ToolAnnotations,
Resource,
ResourceTemplate,
Prompt,
PromptArgument,
PromptMessage,
Annotations,
Role,
);
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tool_widens_to_draft_wire() {
let neutral = Tool::new("echo", json!({"type": "object", "properties": {}}))
.with_description("Echoes input");
let wire: draft::Tool = neutral.into();
assert_eq!(wire.name, "echo");
assert_eq!(wire.input_schema.type_, "object");
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["name"], "echo");
assert_eq!(v["inputSchema"]["type"], "object");
}
#[test]
fn call_result_carries_result_type_and_is_error() {
let wire: draft::CallToolResult = CallToolResult::error("boom").into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["resultType"], "complete");
assert_eq!(v["isError"], true);
assert_eq!(v["content"][0]["type"], "text");
assert_eq!(v["content"][0]["text"], "boom");
}
#[test]
fn list_result_fills_draft_required_fields() {
let wire: draft::ListToolsResult =
ListToolsResult::new(alloc::vec![Tool::new("a", json!({"type": "object"}))]).into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["resultType"], "complete");
assert_eq!(v["cacheScope"], "private");
assert_eq!(v["ttlMs"], 0);
assert_eq!(v["tools"][0]["name"], "a");
}
#[test]
fn read_resource_text_widens_to_wire_union() {
let wire: draft::ReadResourceResult = ReadResourceResult::text("file://a", "hi").into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["resultType"], "complete");
assert_eq!(v["cacheScope"], "private");
assert_eq!(v["contents"][0]["uri"], "file://a");
assert_eq!(v["contents"][0]["text"], "hi");
}
#[test]
fn list_resources_and_templates_fill_required_fields() {
let res: draft::ListResourcesResult = ListResourcesResult::new(alloc::vec![
Resource::new("file://a", "a").with_mime_type("text/plain"),
])
.into();
let v = serde_json::to_value(&res).unwrap();
assert_eq!(v["resultType"], "complete");
assert_eq!(v["resources"][0]["mimeType"], "text/plain");
let templates: draft::ListResourceTemplatesResult = ListResourceTemplatesResult::new(
alloc::vec![ResourceTemplate::new("file://{path}", "files",)],
)
.into();
let v = serde_json::to_value(&templates).unwrap();
assert_eq!(v["resourceTemplates"][0]["uriTemplate"], "file://{path}");
assert_eq!(v["cacheScope"], "private");
}
#[test]
fn prompt_get_widens_with_roles() {
let wire: draft::GetPromptResult = GetPromptResult::new(alloc::vec![
PromptMessage::user_text("hello"),
PromptMessage::assistant_text("hi there"),
])
.with_description("greeting")
.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["resultType"], "complete");
assert_eq!(v["description"], "greeting");
assert_eq!(v["messages"][0]["role"], "user");
assert_eq!(v["messages"][0]["content"]["text"], "hello");
assert_eq!(v["messages"][1]["role"], "assistant");
}
#[test]
fn list_prompts_carries_arguments() {
let wire: draft::ListPromptsResult = ListPromptsResult::new(alloc::vec![
Prompt::new("summarize")
.with_description("Summarize text")
.with_argument(PromptArgument::new("text").required(true)),
])
.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["prompts"][0]["name"], "summarize");
assert_eq!(v["prompts"][0]["arguments"][0]["name"], "text");
assert_eq!(v["prompts"][0]["arguments"][0]["required"], true);
}
#[test]
fn complete_result_nests_completion() {
let wire: draft::CompleteResult = CompleteResult::new(alloc::vec!["foo".to_string()])
.with_total(1)
.with_has_more(false)
.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["resultType"], "complete");
assert_eq!(v["completion"]["values"][0], "foo");
assert_eq!(v["completion"]["total"], 1);
assert_eq!(v["completion"]["hasMore"], false);
}
#[test]
fn legacy_tool_widens_without_draft_envelope() {
let wire: legacy::ListToolsResult = ListToolsResult::new(alloc::vec![
Tool::new(
"echo",
json!({"type": "object", "properties": {"msg": {"type": "string"}}, "required": ["msg"]}),
)
.with_description("Echoes input"),
])
.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["tools"][0]["name"], "echo");
assert_eq!(v["tools"][0]["inputSchema"]["type"], "object");
assert_eq!(
v["tools"][0]["inputSchema"]["properties"]["msg"]["type"],
"string"
);
assert_eq!(v["tools"][0]["inputSchema"]["required"][0], "msg");
let obj = v.as_object().unwrap();
assert!(!obj.contains_key("resultType"));
assert!(!obj.contains_key("cacheScope"));
assert!(!obj.contains_key("ttlMs"));
}
#[test]
fn legacy_call_result_keeps_object_structured_content_drops_non_object() {
let mut ok = CallToolResult::text("done");
ok.structured_content = Some(json!({"answer": 42}));
let wire: legacy::CallToolResult = ok.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["isError"], false);
assert_eq!(v["content"][0]["text"], "done");
assert_eq!(v["structuredContent"]["answer"], 42);
let mut bad = CallToolResult::text("done");
bad.structured_content = Some(json!(7)); let wire: legacy::CallToolResult = bad.into();
let v = serde_json::to_value(&wire).unwrap();
assert!(v.as_object().unwrap().get("structuredContent").is_none());
}
#[test]
fn legacy_resources_round_trip() {
let wire: legacy::ListResourcesResult = ListResourcesResult::new(alloc::vec![
Resource::new("file://a", "a").with_mime_type("text/plain"),
])
.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["resources"][0]["uri"], "file://a");
assert_eq!(v["resources"][0]["mimeType"], "text/plain");
let wire: legacy::ReadResourceResult = ReadResourceResult::text("file://a", "hi").into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["contents"][0]["text"], "hi");
let wire: legacy::ListResourceTemplatesResult = ListResourceTemplatesResult::new(
alloc::vec![ResourceTemplate::new("file://{path}", "files")],
)
.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["resourceTemplates"][0]["uriTemplate"], "file://{path}");
}
#[test]
fn legacy_prompts_and_completion_round_trip() {
let wire: legacy::ListPromptsResult = ListPromptsResult::new(alloc::vec![
Prompt::new("summarize").with_argument(PromptArgument::new("text").required(true)),
])
.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["prompts"][0]["arguments"][0]["required"], true);
let wire: legacy::GetPromptResult =
GetPromptResult::new(alloc::vec![PromptMessage::user_text("hello")]).into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["messages"][0]["role"], "user");
assert_eq!(v["messages"][0]["content"]["type"], "text");
let wire: legacy::CompleteResult = CompleteResult::new(alloc::vec!["x".to_string()])
.with_total(5)
.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["completion"]["total"], 5);
assert_eq!(v["completion"]["values"][0], "x");
}
#[test]
fn draft_tools_round_trip_through_neutral() {
let original = ListToolsResult::new(alloc::vec![
Tool::new("echo", json!({"type": "object"}))
.with_title("Echo")
.with_description("Echoes input"),
]);
let wire: draft::ListToolsResult = original.into();
let back: ListToolsResult = wire.into();
assert_eq!(back.tools.len(), 1);
assert_eq!(back.tools[0].name, "echo");
assert_eq!(back.tools[0].title.as_deref(), Some("Echo"));
assert_eq!(back.tools[0].description.as_deref(), Some("Echoes input"));
assert_eq!(back.tools[0].input_schema["type"], "object");
}
#[test]
fn legacy_tools_round_trip_through_neutral() {
let original =
ListToolsResult::new(alloc::vec![Tool::new("add", json!({"type": "object"}))]);
let wire: legacy::ListToolsResult = original.into();
let back: ListToolsResult = wire.into();
assert_eq!(back.tools[0].name, "add");
}
fn metadata_tool() -> Tool {
Tool::new("audit", json!({"type": "object"}))
.with_title("Audit")
.with_annotations(
ToolAnnotations::new()
.read_only()
.destructive(false)
.idempotent(true)
.open_world(false),
)
.with_icon(Icon {
src: "https://example.com/audit.png".into(),
mime_type: Some("image/png".into()),
sizes: alloc::vec!["48x48".into()],
theme: Some(IconTheme::Dark),
})
.with_meta_entry("com.example/tags", json!(["read", "safety"]))
}
fn assert_metadata_preserved(back: &Tool) {
let a = back.annotations.as_ref().expect("annotations survive");
assert_eq!(a.read_only_hint, Some(true));
assert_eq!(a.destructive_hint, Some(false));
assert_eq!(a.idempotent_hint, Some(true));
assert_eq!(a.open_world_hint, Some(false));
assert_eq!(back.icons.len(), 1);
assert_eq!(back.icons[0].src, "https://example.com/audit.png");
assert_eq!(back.icons[0].mime_type.as_deref(), Some("image/png"));
assert_eq!(back.icons[0].sizes, alloc::vec!["48x48".to_string()]);
assert_eq!(back.icons[0].theme, Some(IconTheme::Dark));
assert_eq!(back.meta["com.example/tags"], json!(["read", "safety"]));
}
#[test]
fn tool_annotations_icons_and_meta_round_trip_the_draft_wire() {
let wire: draft::Tool = metadata_tool().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["annotations"]["readOnlyHint"], json!(true));
assert_eq!(v["annotations"]["destructiveHint"], json!(false));
assert_eq!(v["annotations"]["idempotentHint"], json!(true));
assert_eq!(v["annotations"]["openWorldHint"], json!(false));
assert_eq!(v["icons"][0]["src"], "https://example.com/audit.png");
assert_eq!(v["icons"][0]["theme"], "dark");
assert_eq!(v["_meta"]["com.example/tags"], json!(["read", "safety"]));
let back: Tool = wire.into();
assert_metadata_preserved(&back);
}
#[test]
fn tool_annotations_icons_and_meta_round_trip_the_legacy_wire() {
let wire: legacy::Tool = metadata_tool().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["annotations"]["readOnlyHint"], json!(true));
assert_eq!(v["icons"][0]["theme"], "dark");
assert_eq!(v["_meta"]["com.example/tags"], json!(["read", "safety"]));
let back: Tool = wire.into();
assert_metadata_preserved(&back);
}
#[test]
fn absent_tool_metadata_stays_absent_on_the_wire() {
let wire: draft::Tool = Tool::new("plain", json!({"type": "object"})).into();
let v = serde_json::to_value(&wire).unwrap();
assert!(v.get("annotations").is_none(), "no annotations key: {v}");
assert!(v.get("icons").is_none(), "no icons key: {v}");
assert!(v.get("_meta").is_none(), "no _meta key: {v}");
}
fn metadata_resource() -> Resource {
Resource::new("mem://doc", "doc")
.with_annotations(
Annotations::new()
.for_audience(Role::User)
.priority(0.75)
.last_modified("2026-07-20T00:00:00Z"),
)
.with_icon(Icon::new("https://example.com/doc.png"))
.with_meta_entry("com.example/tags", json!(["docs"]))
}
fn assert_resource_metadata(back: &Resource) {
let a = back.annotations.as_ref().expect("annotations survive");
assert_eq!(a.audience, alloc::vec![Role::User]);
assert_eq!(a.priority, Some(0.75));
assert_eq!(a.last_modified.as_deref(), Some("2026-07-20T00:00:00Z"));
assert_eq!(back.icons[0].src, "https://example.com/doc.png");
assert_eq!(back.meta["com.example/tags"], json!(["docs"]));
}
#[test]
fn resource_metadata_round_trips_both_wires() {
let draft_wire: draft::Resource = metadata_resource().into();
let v = serde_json::to_value(&draft_wire).unwrap();
assert_eq!(v["annotations"]["audience"], json!(["user"]));
assert_eq!(v["annotations"]["priority"], json!(0.75));
assert_eq!(v["annotations"]["lastModified"], "2026-07-20T00:00:00Z");
assert_eq!(v["_meta"]["com.example/tags"], json!(["docs"]));
let back: Resource = draft_wire.into();
assert_resource_metadata(&back);
let legacy_wire: legacy::Resource = metadata_resource().into();
let back: Resource = legacy_wire.into();
assert_resource_metadata(&back);
}
#[test]
fn resource_template_and_prompt_metadata_round_trip_both_wires() {
let template = ResourceTemplate::new("file://{path}", "files")
.with_annotations(Annotations::new().for_audience(Role::Assistant))
.with_meta_entry("com.example/kind", json!("fs"));
let draft_wire: draft::ResourceTemplate = template.clone().into();
let back: ResourceTemplate = draft_wire.into();
assert_eq!(
back.annotations.as_ref().unwrap().audience,
alloc::vec![Role::Assistant]
);
assert_eq!(back.meta["com.example/kind"], json!("fs"));
let legacy_wire: legacy::ResourceTemplate = template.into();
let back: ResourceTemplate = legacy_wire.into();
assert_eq!(back.meta["com.example/kind"], json!("fs"));
let prompt = Prompt::new("summarize")
.with_icon(Icon::new("https://example.com/p.png"))
.with_meta_entry("com.example/category", json!("text"));
let draft_wire: draft::Prompt = prompt.clone().into();
let back: Prompt = draft_wire.into();
assert_eq!(back.icons[0].src, "https://example.com/p.png");
assert_eq!(back.meta["com.example/category"], json!("text"));
let legacy_wire: legacy::Prompt = prompt.into();
let back: Prompt = legacy_wire.into();
assert_eq!(back.icons[0].src, "https://example.com/p.png");
}
#[test]
fn resource_link_content_carries_metadata_both_wires() {
let content = Content::ResourceLink(Box::new(metadata_resource()));
let wire: draft::ContentBlock = content.clone().into();
let back: Content = wire.into();
let Content::ResourceLink(r) = back else {
panic!("resource link survives");
};
assert_resource_metadata(&r);
let wire: legacy::ContentBlock = content.into();
let back: Content = wire.into();
let Content::ResourceLink(r) = back else {
panic!("resource link survives");
};
assert_resource_metadata(&r);
}
#[test]
fn content_block_annotations_and_meta_round_trip_both_wires() {
let annotations = Annotations::new()
.for_audience(Role::User)
.priority(0.5)
.last_modified("2026-07-21T00:00:00Z");
for content in [
Content::text("hi"),
Content::image("aGk=", "image/png"),
Content::audio("aGk=", "audio/wav"),
] {
let content = content
.with_annotations(annotations.clone())
.with_meta_entry("com.example/source", json!("cache"));
let wire: draft::ContentBlock = content.clone().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["annotations"]["audience"], json!(["user"]), "{v}");
assert_eq!(v["annotations"]["priority"], json!(0.5));
assert_eq!(v["annotations"]["lastModified"], "2026-07-21T00:00:00Z");
assert_eq!(v["_meta"]["com.example/source"], json!("cache"));
let back: Content = wire.into();
assert_eq!(back, content);
let wire: legacy::ContentBlock = content.clone().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["annotations"]["priority"], json!(0.5));
assert_eq!(v["_meta"]["com.example/source"], json!("cache"));
let back: Content = wire.into();
assert_eq!(back, content);
}
}
#[test]
fn embedded_resource_block_and_contents_meta_round_trip_both_wires() {
let contents = ResourceContents::text("ui://app", "<html></html>")
.with_mime_type("text/html;profile=mcp-app")
.with_meta_entry("io.modelcontextprotocol/ui", json!({"prefersBorder": true}));
let content = Content::resource(contents)
.with_annotations(Annotations::new().for_audience(Role::User))
.with_meta_entry("com.example/origin", json!("embedded"));
let wire: draft::ContentBlock = content.clone().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["_meta"]["com.example/origin"], json!("embedded"));
assert_eq!(
v["resource"]["_meta"]["io.modelcontextprotocol/ui"]["prefersBorder"],
json!(true)
);
let back: Content = wire.into();
assert_eq!(back, content);
let wire: legacy::ContentBlock = content.clone().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["_meta"]["com.example/origin"], json!("embedded"));
assert_eq!(
v["resource"]["_meta"]["io.modelcontextprotocol/ui"]["prefersBorder"],
json!(true)
);
let back: Content = wire.into();
assert_eq!(back, content);
}
#[test]
fn read_resource_contents_meta_round_trips_both_wires() {
let make = || {
ReadResourceResult::new(alloc::vec![
ResourceContents::text("file://a", "hi")
.with_meta_entry("com.example/etag", json!("abc")),
ResourceContents::blob("file://b", "Zm9v")
.with_meta_entry("com.example/etag", json!("def")),
])
};
let assert_meta = |back: &ReadResourceResult| {
let (ResourceContents::Text { meta, .. }, ResourceContents::Blob { meta: bmeta, .. }) =
(&back.contents[0], &back.contents[1])
else {
panic!("variants survive");
};
assert_eq!(meta["com.example/etag"], json!("abc"));
assert_eq!(bmeta["com.example/etag"], json!("def"));
};
let wire: draft::ReadResourceResult = make().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["contents"][0]["_meta"]["com.example/etag"], json!("abc"));
let back: ReadResourceResult = wire.into();
assert_meta(&back);
let wire: legacy::ReadResourceResult = make().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["contents"][1]["_meta"]["com.example/etag"], json!("def"));
let back: ReadResourceResult = wire.into();
assert_meta(&back);
}
#[test]
fn absent_content_metadata_stays_absent_on_the_wire() {
let wire: draft::ContentBlock = Content::text("plain").into();
let v = serde_json::to_value(&wire).unwrap();
assert!(v.get("annotations").is_none(), "no annotations key: {v}");
assert!(v.get("_meta").is_none(), "no _meta key: {v}");
let wire: legacy::ContentBlock = Content::text("plain").into();
let v = serde_json::to_value(&wire).unwrap();
assert!(v.get("annotations").is_none(), "no annotations key: {v}");
assert!(v.get("_meta").is_none(), "no _meta key: {v}");
let wire: draft::ReadResourceResult = ReadResourceResult::text("file://a", "hi").into();
let v = serde_json::to_value(&wire).unwrap();
assert!(v["contents"][0].get("_meta").is_none(), "no _meta key: {v}");
}
#[test]
fn draft_call_result_round_trips_content_and_is_error() {
let original = CallToolResult::error("boom");
let wire: draft::CallToolResult = original.into();
let back: CallToolResult = wire.into();
assert!(back.is_error);
assert_eq!(back.content.len(), 1);
assert!(matches!(&back.content[0], Content::Text { text, .. } if text == "boom"));
}
#[test]
fn legacy_call_result_object_structured_content_round_trips() {
let mut original = CallToolResult::text("ok");
original.structured_content = Some(json!({"answer": 42}));
let wire: legacy::CallToolResult = original.into();
let back: CallToolResult = wire.into();
assert!(!back.is_error);
assert_eq!(back.structured_content, Some(json!({"answer": 42})));
}
#[test]
fn read_resource_round_trips_text_and_blob() {
let original = ReadResourceResult::new(alloc::vec![
ResourceContents::text("file://a", "hi").with_mime_type("text/plain"),
ResourceContents::blob("file://b", "Zm9v"),
]);
let wire: draft::ReadResourceResult = original.into();
let back: ReadResourceResult = wire.into();
assert_eq!(back.contents.len(), 2);
assert!(
matches!(&back.contents[0], ResourceContents::Text { uri, text, mime_type, .. }
if uri == "file://a" && text == "hi" && mime_type.as_deref() == Some("text/plain"))
);
assert!(
matches!(&back.contents[1], ResourceContents::Blob { uri, blob, .. }
if uri == "file://b" && blob == "Zm9v")
);
}
#[test]
fn prompts_and_completion_round_trip() {
let prompts = ListPromptsResult::new(alloc::vec![
Prompt::new("summarize")
.with_description("Summarize text")
.with_argument(PromptArgument::new("text").required(true)),
]);
let wire: draft::ListPromptsResult = prompts.into();
let back: ListPromptsResult = wire.into();
assert_eq!(back.prompts[0].name, "summarize");
assert!(back.prompts[0].arguments[0].required);
let get = GetPromptResult::new(alloc::vec![PromptMessage::user_text("hello")])
.with_description("greeting");
let wire: legacy::GetPromptResult = get.into();
let back: GetPromptResult = wire.into();
assert_eq!(back.description.as_deref(), Some("greeting"));
assert!(matches!(&back.messages[0].content, Content::Text { text, .. } if text == "hello"));
assert!(matches!(back.messages[0].role, Role::User));
let complete = CompleteResult::new(alloc::vec!["foo".to_string()])
.with_total(1)
.with_has_more(false);
let wire: draft::CompleteResult = complete.into();
let back: CompleteResult = wire.into();
assert_eq!(back.values, alloc::vec!["foo".to_string()]);
assert_eq!(back.total, Some(1));
assert_eq!(back.has_more, Some(false));
}
#[test]
fn image_and_audio_content_round_trip() {
for content in [
Content::image("Zm9v", "image/png"),
Content::audio("YmFy", "audio/wav"),
] {
let draft_block: draft::ContentBlock = content.clone().into();
assert_eq!(Content::from(draft_block), content);
let legacy_block: legacy::ContentBlock = content.clone().into();
assert_eq!(Content::from(legacy_block), content);
}
}
#[test]
fn resource_and_resource_link_content_round_trip() {
for content in [
Content::resource(
ResourceContents::text("file://x", "hi").with_mime_type("text/plain"),
),
Content::resource(
ResourceContents::blob("file://y", "Zm9v").with_mime_type("image/png"),
),
Content::resource_link(
Resource::new("file://x", "x")
.with_title("X")
.with_mime_type("text/plain"),
),
] {
let draft_block: draft::ContentBlock = content.clone().into();
assert_eq!(Content::from(draft_block), content);
let legacy_block: legacy::ContentBlock = content.clone().into();
assert_eq!(Content::from(legacy_block), content);
}
}
#[test]
fn task_support_round_trips_the_legacy_wire_and_drops_on_draft() {
for (ts, wire_str) in [
(TaskSupport::Forbidden, "forbidden"),
(TaskSupport::Optional, "optional"),
(TaskSupport::Required, "required"),
] {
let make = || Tool::new("t", json!({"type": "object"})).with_task_support(ts);
let wire: legacy::Tool = make().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["execution"]["taskSupport"], wire_str);
let back: Tool = wire.into();
assert_eq!(back.task_support, Some(ts));
let wire: draft::Tool = make().into();
let v = serde_json::to_value(&wire).unwrap();
assert!(v.get("execution").is_none(), "no draft execution key: {v}");
let back: Tool = wire.into();
assert_eq!(back.task_support, None);
}
let wire: legacy::Tool = Tool::new("t", json!({"type": "object"})).into();
let back: Tool = wire.into();
assert_eq!(back.task_support, None);
}
#[test]
fn draft_structured_content_keeps_non_object_values() {
for sc in [json!(7), json!([1, 2, 3]), json!("str"), json!(true)] {
let mut r = CallToolResult::text("ok");
r.structured_content = Some(sc.clone());
let wire: draft::CallToolResult = r.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["structuredContent"], sc);
let back: CallToolResult = wire.into();
assert_eq!(back.structured_content, Some(sc));
}
}
#[test]
fn cache_policy_round_trips_the_draft_wire_and_drops_on_legacy() {
let result = ListToolsResult::new(alloc::vec![])
.with_cache(CachePolicy::public(core::time::Duration::from_secs(60)));
let wire: draft::ListToolsResult = result.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["ttlMs"], 60_000);
assert_eq!(v["cacheScope"], "public");
let back: ListToolsResult = wire.into();
assert_eq!(
back.cache,
Some(CachePolicy::from_wire(60_000, CacheScope::Public))
);
let rr = ReadResourceResult::text("file://a", "hi")
.with_cache(CachePolicy::private(core::time::Duration::from_millis(500)));
let wire: draft::ReadResourceResult = rr.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["ttlMs"], 500);
assert_eq!(v["cacheScope"], "private");
let back: ReadResourceResult = wire.into();
assert_eq!(
back.cache,
Some(CachePolicy::from_wire(500, CacheScope::Private))
);
let result = ListToolsResult::new(alloc::vec![])
.with_cache(CachePolicy::public(core::time::Duration::from_secs(60)));
let wire: legacy::ListToolsResult = result.into();
let back: ListToolsResult = wire.into();
assert_eq!(back.cache, None);
let wire: draft::ListToolsResult = ListToolsResult::new(alloc::vec![]).into();
let back: ListToolsResult = wire.into();
assert_eq!(back.cache, Some(CachePolicy::NO_CACHE));
}
#[test]
fn legacy_annotations_wire_names_are_exact() {
let wire: legacy::Resource = metadata_resource().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["annotations"]["audience"], json!(["user"]));
assert_eq!(v["annotations"]["priority"], json!(0.75));
assert_eq!(v["annotations"]["lastModified"], "2026-07-20T00:00:00Z");
let content = Content::text("hi").with_annotations(
Annotations::new()
.for_audience(Role::User)
.last_modified("2026-07-21T00:00:00Z"),
);
let wire: legacy::ContentBlock = content.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["annotations"]["audience"], json!(["user"]));
assert_eq!(v["annotations"]["lastModified"], "2026-07-21T00:00:00Z");
}
#[test]
fn absent_tool_metadata_stays_absent_on_the_legacy_wire() {
let wire: legacy::Tool = Tool::new("plain", json!({"type": "object"})).into();
let v = serde_json::to_value(&wire).unwrap();
assert!(v.get("annotations").is_none(), "no annotations key: {v}");
assert!(v.get("icons").is_none(), "no icons key: {v}");
assert!(v.get("_meta").is_none(), "no _meta key: {v}");
assert!(v.get("execution").is_none(), "no execution key: {v}");
}
#[test]
fn resource_size_round_trips_and_clamps() {
let mut resource = Resource::new("file://big", "big");
resource.size = Some(4096);
for wire_v in [
serde_json::to_value(draft::Resource::from(resource.clone())).unwrap(),
serde_json::to_value(legacy::Resource::from(resource.clone())).unwrap(),
] {
assert_eq!(wire_v["size"], 4096);
}
let back: Resource = draft::Resource::from(resource.clone()).into();
assert_eq!(back.size, Some(4096));
let back: Resource = legacy::Resource::from(resource.clone()).into();
assert_eq!(back.size, Some(4096));
resource.size = Some(u64::MAX);
let back: Resource = draft::Resource::from(resource.clone()).into();
assert_eq!(back.size, Some(u64::try_from(i64::MAX).unwrap()));
let wire: draft::ContentBlock = Content::resource_link(resource).into();
let Content::ResourceLink(r) = Content::from(wire) else {
panic!("resource link survives");
};
assert_eq!(r.size, Some(u64::try_from(i64::MAX).unwrap()));
}
#[test]
fn icon_theme_light_round_trips_both_wires() {
let tool = Tool::new("t", json!({"type": "object"})).with_icon(Icon {
src: "https://example.com/i.png".into(),
mime_type: None,
sizes: alloc::vec![],
theme: Some(IconTheme::Light),
});
let wire: draft::Tool = tool.clone().into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["icons"][0]["theme"], "light");
let back: Tool = wire.into();
assert_eq!(back.icons[0].theme, Some(IconTheme::Light));
let wire: legacy::Tool = tool.into();
let v = serde_json::to_value(&wire).unwrap();
assert_eq!(v["icons"][0]["theme"], "light");
let back: Tool = wire.into();
assert_eq!(back.icons[0].theme, Some(IconTheme::Light));
}
#[test]
fn empty_annotations_serialize_as_empty_object_not_empty_arrays() {
let content = Content::text("hi").with_annotations(Annotations::new());
for v in [
serde_json::to_value(draft::ContentBlock::from(content.clone())).unwrap(),
serde_json::to_value(legacy::ContentBlock::from(content.clone())).unwrap(),
] {
assert_eq!(v["annotations"], json!({}), "{v}");
}
let wire: draft::ContentBlock = content.clone().into();
let back: Content = wire.into();
assert_eq!(back, content);
}
}