🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

rmcp

Package Overview
Dependencies
Maintainers
1
Versions
50
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

rmcp - cargo Package Compare versions

Comparing version
1.8.0
to
2.0.0
+1
-1
.cargo_vcs_info.json
{
"git": {
"sha1": "25220361d5540715294c501c289d79de4bec2bfc"
"sha1": "67a30859443ab0fe79f2d50307c7d7bc9518f7e3"
},
"path_in_vcs": "crates/rmcp"
}

@@ -15,3 +15,3 @@ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO

name = "rmcp"
version = "1.8.0"
version = "2.0.0"
build = "build.rs"

@@ -587,3 +587,3 @@ autolib = false

[dependencies.rmcp-macros]
version = "1.8.0"
version = "2.0.0"
optional = true

@@ -674,2 +674,5 @@

[dev-dependencies.rstest]
version = "0.26.1"
[dev-dependencies.schemars]

@@ -676,0 +679,0 @@ version = "1.1.0"

@@ -10,2 +10,24 @@ # Changelog

## [2.0.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-v1.8.0...rmcp-v2.0.0) - 2026-06-27
### Added
- [**breaking**] relax tool result structuredContent type ([#919](https://github.com/modelcontextprotocol/rust-sdk/pull/919))
- deprecate roots/sampling/logging types ([#923](https://github.com/modelcontextprotocol/rust-sdk/pull/923))
- [**breaking**] align model types with MCP 2025-11-25 spec ([#927](https://github.com/modelcontextprotocol/rust-sdk/pull/927))
### Fixed
- prevent OAuth resource spoofing ([#937](https://github.com/modelcontextprotocol/rust-sdk/pull/937))
- block oauth metadata ssrf ([#935](https://github.com/modelcontextprotocol/rust-sdk/pull/935))
- prevent streamable HTTP session leak ([#934](https://github.com/modelcontextprotocol/rust-sdk/pull/934))
- fill missing fully qualified syntax in prompt_handler macros ([#866](https://github.com/modelcontextprotocol/rust-sdk/pull/866))
- *(rmcp)* add Audio variant to PromptMessageContent ([#865](https://github.com/modelcontextprotocol/rust-sdk/pull/865))
### Other
- consolidate repeated rmcp tests ([#931](https://github.com/modelcontextprotocol/rust-sdk/pull/931))
- Revert "feat!: relax tool result structuredContent type ([#919](https://github.com/modelcontextprotocol/rust-sdk/pull/919))" ([#932](https://github.com/modelcontextprotocol/rust-sdk/pull/932))
- align README examples with v2 model API ([#928](https://github.com/modelcontextprotocol/rust-sdk/pull/928))
## [1.8.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-v1.7.0...rmcp-v1.8.0) - 2026-06-22

@@ -12,0 +34,0 @@

@@ -0,1 +1,3 @@

// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
#![expect(deprecated)]
pub mod progress;

@@ -29,6 +31,6 @@ use std::sync::Arc;

.map(ClientResult::ListRootsResult),
ServerRequest::CreateElicitationRequest(request) => self
ServerRequest::ElicitRequest(request) => self
.create_elicitation(request.params, context)
.await
.map(ClientResult::CreateElicitationResult),
.map(ClientResult::ElicitResult),
ServerRequest::CustomRequest(request) => self

@@ -68,6 +70,9 @@ .on_custom_request(request, context)

}
ServerNotification::ElicitationCompletionNotification(notification) => {
ServerNotification::ElicitationCompleteNotification(notification) => {
self.on_url_elicitation_notification_complete(notification.params, context)
.await
}
ServerNotification::TaskStatusNotification(notification) => {
self.on_task_status(notification.params, context).await
}
ServerNotification::CustomNotification(notification) => {

@@ -130,3 +135,3 @@ self.on_custom_notification(notification, context).await

/// ```rust,ignore
/// use rmcp::model::CreateElicitationRequestParam;
/// use rmcp::model::ElicitRequestParams;
/// use rmcp::{

@@ -142,10 +147,10 @@ /// model::ErrorData as McpError,

/// &self,
/// request: CreateElicitationRequestParam,
/// request: ElicitRequestParams,
/// context: RequestContext<RoleClient>,
/// ) -> Result<CreateElicitationResult, McpError> {
/// ) -> Result<ElicitResult, McpError> {
/// match request {
/// CreateElicitationRequestParam::FormElicitationParam {meta, message, requested_schema,} => {
/// ElicitRequestParams::FormElicitationParam {meta, message, requested_schema,} => {
/// // Display message to user and collect input according to requested_schema
/// let user_input = get_user_input(message, requested_schema).await?;
/// Ok(CreateElicitationResult {
/// Ok(ElicitResult {
/// action: ElicitationAction::Accept,

@@ -156,6 +161,6 @@ /// content: Some(user_input),

/// }
/// CreateElicitationRequestParam::UrlElicitationParam {meta, message, url, elicitation_id,} => {
/// ElicitRequestParams::UrlElicitationParam {meta, message, url, elicitation_id,} => {
/// // Open URL in browser for user to complete elicitation
/// open_url_in_browser(url).await?;
/// Ok(CreateElicitationResult {
/// Ok(ElicitResult {
/// action: ElicitationAction::Accept,

@@ -172,9 +177,8 @@ /// content: None,

&self,
request: CreateElicitationRequestParams,
request: ElicitRequestParams,
context: RequestContext<RoleClient>,
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + MaybeSendFuture + '_
{
) -> impl Future<Output = Result<ElicitResult, McpError>> + MaybeSendFuture + '_ {
// Default implementation declines all requests - real clients should override this
let _ = (request, context);
std::future::ready(Ok(CreateElicitationResult {
std::future::ready(Ok(ElicitResult {
action: ElicitationAction::Decline,

@@ -254,2 +258,9 @@ content: None,

}
fn on_task_status(
&self,
params: TaskStatusNotificationParam,
context: NotificationContext<RoleClient>,
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
std::future::ready(())
}
fn on_custom_notification(

@@ -293,3 +304,4 @@ &self,

context: RequestContext<RoleClient>,
) -> impl Future<Output = Result<CreateMessageResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<CreateMessageResult, McpError>> + MaybeSendFuture + '_
{
(**self).create_message(params, context)

@@ -301,3 +313,4 @@ }

context: RequestContext<RoleClient>,
) -> impl Future<Output = Result<ListRootsResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<ListRootsResult, McpError>> + MaybeSendFuture + '_
{
(**self).list_roots(context)

@@ -308,5 +321,5 @@ }

&self,
request: CreateElicitationRequestParams,
request: ElicitRequestParams,
context: RequestContext<RoleClient>,
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<ElicitResult, McpError>> + MaybeSendFuture + '_ {
(**self).create_elicitation(request, context)

@@ -376,2 +389,10 @@ }

fn on_task_status(
&self,
params: TaskStatusNotificationParam,
context: NotificationContext<RoleClient>,
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
(**self).on_task_status(params, context)
}
fn on_custom_notification(

@@ -378,0 +399,0 @@ &self,

@@ -0,1 +1,3 @@

// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
#![expect(deprecated)]
use std::sync::Arc;

@@ -120,7 +122,7 @@

.map(ServerResult::ListTasksResult),
ClientRequest::GetTaskInfoRequest(request) => self
ClientRequest::GetTaskRequest(request) => self
.get_task_info(request.params, context)
.await
.map(ServerResult::GetTaskResult),
ClientRequest::GetTaskResultRequest(request) => self
ClientRequest::GetTaskPayloadRequest(request) => self
.get_task_result(request.params, context)

@@ -165,2 +167,5 @@ .await

}
ClientNotification::TaskStatusNotification(notification) => {
self.on_task_status(notification.params, context).await
}
ClientNotification::CustomNotification(notification) => {

@@ -364,2 +369,9 @@ self.on_custom_notification(notification, context).await

}
fn on_task_status(
&self,
params: TaskStatusNotificationParam,
context: NotificationContext<RoleServer>,
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
std::future::ready(())
}
fn on_custom_notification(

@@ -388,7 +400,7 @@ &self,

&self,
request: GetTaskInfoParams,
request: GetTaskParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetTaskResult, McpError>> + MaybeSendFuture + '_ {
let _ = (request, context);
std::future::ready(Err(McpError::method_not_found::<GetTaskInfoMethod>()))
std::future::ready(Err(McpError::method_not_found::<GetTaskMethod>()))
}

@@ -398,7 +410,7 @@

&self,
request: GetTaskResultParams,
request: GetTaskPayloadParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetTaskPayloadResult, McpError>> + MaybeSendFuture + '_ {
let _ = (request, context);
std::future::ready(Err(McpError::method_not_found::<GetTaskResultMethod>()))
std::future::ready(Err(McpError::method_not_found::<GetTaskPayloadMethod>()))
}

@@ -586,2 +598,10 @@

fn on_task_status(
&self,
params: TaskStatusNotificationParam,
context: NotificationContext<RoleServer>,
) -> impl Future<Output = ()> + MaybeSendFuture + '_ {
(**self).on_task_status(params, context)
}
fn on_custom_notification(

@@ -609,3 +629,3 @@ &self,

&self,
request: GetTaskInfoParams,
request: GetTaskParams,
context: RequestContext<RoleServer>,

@@ -618,3 +638,3 @@ ) -> impl Future<Output = Result<GetTaskResult, McpError>> + MaybeSendFuture + '_ {

&self,
request: GetTaskResultParams,
request: GetTaskPayloadParams,
context: RequestContext<RoleServer>,

@@ -621,0 +641,0 @@ ) -> impl Future<Output = Result<GetTaskPayloadResult, McpError>> + MaybeSendFuture + '_ {

@@ -236,2 +236,4 @@ //! Common utilities shared between tool and prompt handlers

mod tests {
use rstest::rstest;
use super::*;

@@ -249,34 +251,40 @@

#[test]
fn test_schema_for_type_handles_primitive() {
let schema = schema_for_type::<i32>();
#[rstest]
#[case::primitive(schema_for_type::<i32>, "integer")]
#[case::array(schema_for_type::<Vec<i32>>, "array")]
#[case::struct_object(schema_for_type::<TestObject>, "object")]
fn schema_for_type_sets_expected_root_type(
#[case] schema_fn: fn() -> Arc<JsonObject>,
#[case] expected_type: &str,
) {
let schema = schema_fn();
assert_eq!(schema.get("type"), Some(&serde_json::json!("integer")));
assert_eq!(schema.get("type"), Some(&serde_json::json!(expected_type)));
}
#[test]
fn test_schema_for_type_handles_array() {
fn schema_for_type_sets_array_item_type() {
let schema = schema_for_type::<Vec<i32>>();
let items = schema.get("items").and_then(|v| v.as_object()).unwrap();
assert_eq!(schema.get("type"), Some(&serde_json::json!("array")));
let items = schema.get("items").and_then(|v| v.as_object());
assert_eq!(
items.unwrap().get("type"),
Some(&serde_json::json!("integer"))
);
assert_eq!(items.get("type"), Some(&serde_json::json!("integer")));
}
#[test]
fn test_schema_for_type_handles_struct() {
fn schema_for_type_sets_struct_properties() {
let schema = schema_for_type::<TestObject>();
let properties = schema
.get("properties")
.and_then(|v| v.as_object())
.unwrap();
assert_eq!(schema.get("type"), Some(&serde_json::json!("object")));
let properties = schema.get("properties").and_then(|v| v.as_object());
assert!(properties.unwrap().contains_key("value"));
assert!(properties.contains_key("value"));
}
#[test]
fn test_schema_for_type_caches_primitive_types() {
let schema1 = schema_for_type::<i32>();
let schema2 = schema_for_type::<i32>();
#[rstest]
#[case::primitive(schema_for_type::<i32>)]
#[case::struct_object(schema_for_type::<TestObject>)]
fn test_schema_for_type_caches_schemas(#[case] schema_fn: fn() -> Arc<JsonObject>) {
let schema1 = schema_fn();
let schema2 = schema_fn();

@@ -287,10 +295,2 @@ assert!(Arc::ptr_eq(&schema1, &schema2));

#[test]
fn test_schema_for_type_caches_struct_types() {
let schema1 = schema_for_type::<TestObject>();
let schema2 = schema_for_type::<TestObject>();
assert!(Arc::ptr_eq(&schema1, &schema2));
}
#[test]
fn test_schema_for_type_different_types_different_schemas() {

@@ -311,49 +311,34 @@ let schema1 = schema_for_type::<TestObject>();

#[test]
fn test_schema_for_output_rejects_primitive() {
let result = schema_for_output::<i32>();
assert!(result.is_err(),);
}
#[test]
fn test_schema_for_output_accepts_object() {
let result = schema_for_output::<TestObject>();
assert!(result.is_ok(),);
}
#[test]
fn test_schema_for_output_strips_top_level_title() {
let schema = schema_for_output::<TestObject>().unwrap();
assert!(!schema.contains_key("title"));
}
#[test]
fn test_schema_for_output_strips_top_level_description() {
let schema = schema_for_output::<TestObject>().unwrap();
assert!(!schema.contains_key("description"));
}
#[test]
fn test_schema_for_input_rejects_primitive() {
let result = schema_for_input::<i32>();
#[rstest]
#[case::output(schema_for_output::<i32>)]
#[case::input(schema_for_input::<i32>)]
fn test_schema_for_object_wrappers_reject_primitives(
#[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
) {
let result = schema_fn();
assert!(result.is_err());
}
#[test]
fn test_schema_for_input_accepts_object() {
let result = schema_for_input::<TestObject>();
#[rstest]
#[case::output(schema_for_output::<TestObject>)]
#[case::input(schema_for_input::<TestObject>)]
fn test_schema_for_object_wrappers_accept_objects(
#[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
) {
let result = schema_fn();
assert!(result.is_ok());
}
#[test]
fn test_schema_for_input_strips_top_level_title() {
let schema = schema_for_input::<TestObject>().unwrap();
assert!(!schema.contains_key("title"));
#[rstest]
#[case::output_title(schema_for_output::<TestObject>, "title")]
#[case::output_description(schema_for_output::<TestObject>, "description")]
#[case::input_title(schema_for_input::<TestObject>, "title")]
#[case::input_description(schema_for_input::<TestObject>, "description")]
fn test_schema_for_object_wrappers_strip_top_level_metadata(
#[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
#[case] field: &str,
) {
let schema = schema_fn().unwrap();
assert!(!schema.contains_key(field));
}
#[test]
fn test_schema_for_input_strips_top_level_description() {
let schema = schema_for_input::<TestObject>().unwrap();
assert!(!schema.contains_key("description"));
}
}

@@ -108,2 +108,3 @@ //! Prompt handling infrastructure for MCP servers

messages: self,
meta: None,
})

@@ -110,0 +111,0 @@ }

@@ -140,3 +140,3 @@ //! Tools for MCP servers.

},
model::{CallToolResult, Content, ErrorCode, Tool, ToolAnnotations},
model::{CallToolResult, ContentBlock, ErrorCode, Tool, ToolAnnotations},
service::{MaybeBoxFuture, MaybeSend},

@@ -153,3 +153,5 @@ };

{
return Ok(CallToolResult::error(vec![Content::text(error.message)]));
return Ok(CallToolResult::error(vec![ContentBlock::text(
error.message,
)]));
}

@@ -685,3 +687,3 @@

.first()
.and_then(|content| content.raw.as_text())
.and_then(|content| content.as_text())
.map(|text| text.text.as_str())

@@ -688,0 +690,0 @@ .expect("tool error result should include text");

@@ -38,3 +38,3 @@ use std::{

pub arguments: Option<JsonObject>,
pub task: Option<JsonObject>,
pub task: Option<crate::model::TaskMetadata>,
}

@@ -41,0 +41,0 @@

@@ -1,2 +0,5 @@

use std::ops::{Deref, DerefMut};
//! Annotations for content blocks and resources.
//!
//! The `Annotations` struct carries optional hints about audience, priority, and freshness.
//! Individual content/resource types embed `annotations: Option<Annotations>` directly.

@@ -6,7 +9,6 @@ use chrono::{DateTime, Utc};

use super::{
RawAudioContent, RawContent, RawEmbeddedResource, RawImageContent, RawResource,
RawResourceTemplate, RawTextContent, Role,
};
use super::Role;
/// Optional annotations for the client. The client can use annotations to inform how objects are
/// used or displayed.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]

@@ -39,190 +41,21 @@ #[serde(rename_all = "camelCase")]

}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct Annotated<T: AnnotateAble> {
#[serde(flatten)]
pub raw: T,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
impl<T: AnnotateAble> Deref for Annotated<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.raw
pub fn with_audience(mut self, audience: Vec<Role>) -> Self {
self.audience = Some(audience);
self
}
}
impl<T: AnnotateAble> DerefMut for Annotated<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.raw
pub fn with_priority(mut self, priority: f32) -> Self {
self.priority = Some(priority);
self
}
}
impl<T: AnnotateAble> Annotated<T> {
pub fn new(raw: T, annotations: Option<Annotations>) -> Self {
Self { raw, annotations }
pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.last_modified = Some(timestamp);
self
}
pub fn remove_annotation(&mut self) -> Option<Annotations> {
self.annotations.take()
}
pub fn audience(&self) -> Option<&Vec<Role>> {
self.annotations.as_ref().and_then(|a| a.audience.as_ref())
}
pub fn priority(&self) -> Option<f32> {
self.annotations.as_ref().and_then(|a| a.priority)
}
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
self.annotations.as_ref().and_then(|a| a.last_modified)
}
pub fn with_audience(self, audience: Vec<Role>) -> Annotated<T>
where
Self: Sized,
{
if let Some(annotations) = self.annotations {
Annotated {
raw: self.raw,
annotations: Some(Annotations {
audience: Some(audience),
..annotations
}),
}
} else {
Annotated {
raw: self.raw,
annotations: Some(Annotations {
audience: Some(audience),
priority: None,
last_modified: None,
}),
}
}
}
pub fn with_priority(self, priority: f32) -> Annotated<T>
where
Self: Sized,
{
if let Some(annotations) = self.annotations {
Annotated {
raw: self.raw,
annotations: Some(Annotations {
priority: Some(priority),
..annotations
}),
}
} else {
Annotated {
raw: self.raw,
annotations: Some(Annotations {
priority: Some(priority),
last_modified: None,
audience: None,
}),
}
}
}
pub fn with_timestamp(self, timestamp: DateTime<Utc>) -> Annotated<T>
where
Self: Sized,
{
if let Some(annotations) = self.annotations {
Annotated {
raw: self.raw,
annotations: Some(Annotations {
last_modified: Some(timestamp),
..annotations
}),
}
} else {
Annotated {
raw: self.raw,
annotations: Some(Annotations {
last_modified: Some(timestamp),
priority: None,
audience: None,
}),
}
}
}
pub fn with_timestamp_now(self) -> Annotated<T>
where
Self: Sized,
{
self.with_timestamp(Utc::now())
}
}
mod sealed {
pub trait Sealed {}
}
macro_rules! annotate {
($T: ident) => {
impl sealed::Sealed for $T {}
impl AnnotateAble for $T {}
};
}
annotate!(RawContent);
annotate!(RawTextContent);
annotate!(RawImageContent);
annotate!(RawAudioContent);
annotate!(RawEmbeddedResource);
annotate!(RawResource);
annotate!(RawResourceTemplate);
pub trait AnnotateAble: sealed::Sealed {
fn optional_annotate(self, annotations: Option<Annotations>) -> Annotated<Self>
where
Self: Sized,
{
Annotated::new(self, annotations)
}
fn annotate(self, annotations: Annotations) -> Annotated<Self>
where
Self: Sized,
{
Annotated::new(self, Some(annotations))
}
fn no_annotation(self) -> Annotated<Self>
where
Self: Sized,
{
Annotated::new(self, None)
}
fn with_audience(self, audience: Vec<Role>) -> Annotated<Self>
where
Self: Sized,
{
self.annotate(Annotations {
audience: Some(audience),
..Default::default()
})
}
fn with_priority(self, priority: f32) -> Annotated<Self>
where
Self: Sized,
{
self.annotate(Annotations {
priority: Some(priority),
..Default::default()
})
}
fn with_timestamp(self, timestamp: DateTime<Utc>) -> Annotated<Self>
where
Self: Sized,
{
self.annotate(Annotations {
last_modified: Some(timestamp),
..Default::default()
})
}
fn with_timestamp_now(self) -> Annotated<Self>
where
Self: Sized,
{
pub fn with_timestamp_now(self) -> Self {
self.with_timestamp(Utc::now())
}
}

@@ -37,3 +37,3 @@ use std::collections::BTreeMap;

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct PromptsCapability {

@@ -47,3 +47,3 @@ #[serde(skip_serializing_if = "Option::is_none")]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct ResourcesCapability {

@@ -59,3 +59,3 @@ #[serde(skip_serializing_if = "Option::is_none")]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct ToolsCapability {

@@ -72,3 +72,3 @@ #[serde(skip_serializing_if = "Option::is_none")]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct RootsCapabilities {

@@ -83,3 +83,3 @@ #[serde(skip_serializing_if = "Option::is_none")]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct TasksCapability {

@@ -98,3 +98,3 @@ #[serde(skip_serializing_if = "Option::is_none")]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct TaskRequestsCapability {

@@ -115,3 +115,3 @@ #[serde(skip_serializing_if = "Option::is_none")]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct SamplingTaskCapability {

@@ -125,3 +125,3 @@ #[serde(skip_serializing_if = "Option::is_none")]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct ElicitationTaskCapability {

@@ -135,3 +135,3 @@ #[serde(skip_serializing_if = "Option::is_none")]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct ToolsTaskCapability {

@@ -217,3 +217,3 @@ #[serde(skip_serializing_if = "Option::is_none")]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct FormElicitationCapability {

@@ -227,8 +227,25 @@ /// Whether the client supports JSON Schema validation for elicitation responses.

impl FormElicitationCapability {
pub fn new() -> Self {
Self::default()
}
pub fn with_schema_validation(mut self, enabled: bool) -> Self {
self.schema_validation = Some(enabled);
self
}
}
/// Capability for URL mode elicitation.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct UrlElicitationCapability {}
impl UrlElicitationCapability {
pub fn new() -> Self {
Self::default()
}
}
/// Elicitation allows servers to request interactive input from users during tool execution.

@@ -240,3 +257,3 @@ /// This capability indicates that a client can handle elicitation requests and present

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct ElicitationCapability {

@@ -251,2 +268,18 @@ /// Whether client supports form-based elicitation.

impl ElicitationCapability {
pub fn new() -> Self {
Self::default()
}
pub fn with_form(mut self, form: FormElicitationCapability) -> Self {
self.form = Some(form);
self
}
pub fn with_url(mut self, url: UrlElicitationCapability) -> Self {
self.url = Some(url);
self
}
}
/// Sampling capability with optional sub-capabilities (SEP-1577).

@@ -260,3 +293,3 @@ ///

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct SamplingCapability {

@@ -263,0 +296,0 @@ /// Support for `tools` and `toolChoice` parameters

@@ -1,58 +0,160 @@

//! 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
//! Content types that flow between agents, tools, prompts, and LLMs.
//!
//! The core union is [`ContentBlock`] (text | image | audio | resource_link | resource),
//! matching the MCP 2025-11-25 `ContentBlock` definition. Each variant carries optional
//! [`Annotations`] and `_meta` inline.
//!
//! [`SamplingMessageContentBlock`] extends the union with `tool_use` and `tool_result`
//! variants for sampling messages (SEP-1577).
// ToolUseContent/ToolResultContent are SEP-2577-deprecated; internal references are expected.
#![expect(deprecated)]
use serde::{Deserialize, Serialize};
use serde_json::json;
use super::{AnnotateAble, Annotated, resource::ResourceContents};
use super::{Annotations, Meta, resource::ResourceContents};
// ---------------------------------------------------------------------------
// Flat content structs
// ---------------------------------------------------------------------------
/// Text content block (spec `TextContent`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct RawTextContent {
#[non_exhaustive]
pub struct TextContent {
/// The text content of the message.
pub text: String,
/// Optional protocol-level metadata for this content block
/// Optional protocol-level metadata for this content block.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<super::Meta>,
pub meta: Option<Meta>,
/// Optional annotations describing how the client should use this content.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
pub type TextContent = Annotated<RawTextContent>;
impl TextContent {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
meta: None,
annotations: None,
}
}
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
}
/// Image content with base64-encoded data (spec `ImageContent`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct RawImageContent {
/// The base64-encoded image
#[non_exhaustive]
pub struct ImageContent {
/// The base64-encoded image data.
pub data: String,
/// The MIME type of the image (e.g. `image/png`).
pub mime_type: String,
/// Optional protocol-level metadata for this content block
/// Optional protocol-level metadata for this content block.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<super::Meta>,
pub meta: Option<Meta>,
/// Optional annotations describing how the client should use this content.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
pub type ImageContent = Annotated<RawImageContent>;
impl ImageContent {
pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self {
data: data.into(),
mime_type: mime_type.into(),
meta: None,
annotations: None,
}
}
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
}
/// Audio content with base64-encoded data (spec `AudioContent`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct RawEmbeddedResource {
/// Optional protocol-level metadata for this content block
#[non_exhaustive]
pub struct AudioContent {
/// The base64-encoded audio data.
pub data: String,
/// The MIME type of the audio (e.g. `audio/wav`).
pub mime_type: String,
/// Optional protocol-level metadata for this content block.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<super::Meta>,
pub resource: ResourceContents,
pub meta: Option<Meta>,
/// Optional annotations describing how the client should use this content.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
impl RawEmbeddedResource {
/// Create a new RawEmbeddedResource.
pub fn new(resource: ResourceContents) -> Self {
impl AudioContent {
pub fn new(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
Self {
data: data.into(),
mime_type: mime_type.into(),
meta: None,
resource,
annotations: None,
}
}
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
}
pub type EmbeddedResource = Annotated<RawEmbeddedResource>;
/// Embedded resource content (spec `EmbeddedResource`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct EmbeddedResource {
/// The embedded resource contents (text or blob).
pub resource: ResourceContents,
/// Optional protocol-level metadata for this content block.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
/// Optional annotations describing how the client should use this content.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
impl EmbeddedResource {
pub fn new(resource: ResourceContents) -> Self {
Self {
resource,
meta: None,
annotations: None,
}
}
pub fn get_text(&self) -> String {

@@ -64,15 +166,14 @@ match &self.resource {

}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct RawAudioContent {
pub data: String,
pub mime_type: String,
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
}
pub type AudioContent = Annotated<RawAudioContent>;
/// Tool call request from assistant (SEP-1577).

@@ -83,12 +184,12 @@ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

#[non_exhaustive]
#[deprecated(
since = "2.0.0",
note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
)]
pub struct ToolUseContent {
/// Unique identifier for this tool call
pub id: String,
/// Name of the tool to call
pub name: String,
/// Input arguments for the tool
pub input: super::JsonObject,
/// Optional metadata (preserved for caching)
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<super::Meta>,
pub meta: Option<Meta>,
}

@@ -101,15 +202,13 @@

#[non_exhaustive]
#[deprecated(
since = "2.0.0",
note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
)]
pub struct ToolResultContent {
/// Optional metadata
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<super::Meta>,
/// ID of the corresponding tool use
pub meta: Option<Meta>,
pub tool_use_id: String,
/// Content blocks returned by the tool
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub content: Vec<Content>,
/// Optional structured result
pub content: Vec<ContentBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
pub structured_content: Option<super::JsonObject>,
/// Whether tool execution failed
#[serde(skip_serializing_if = "Option::is_none")]

@@ -131,3 +230,3 @@ pub is_error: Option<bool>,

impl ToolResultContent {
pub fn new(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
pub fn new(tool_use_id: impl Into<String>, content: Vec<ContentBlock>) -> Self {
Self {

@@ -142,3 +241,3 @@ meta: None,

pub fn error(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
pub fn error(tool_use_id: impl Into<String>, content: Vec<ContentBlock>) -> Self {
Self {

@@ -154,17 +253,22 @@ meta: None,

// ---------------------------------------------------------------------------
// ContentBlock — the unified content union (spec `ContentBlock`)
// ---------------------------------------------------------------------------
/// Unified content block union (spec `ContentBlock`).
///
/// `text | image | audio | resource_link | resource`
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
pub enum RawContent {
Text(RawTextContent),
Image(RawImageContent),
Resource(RawEmbeddedResource),
Audio(RawAudioContent),
ResourceLink(super::resource::RawResource),
#[non_exhaustive]
pub enum ContentBlock {
Text(TextContent),
Image(ImageContent),
Audio(AudioContent),
Resource(EmbeddedResource),
ResourceLink(super::resource::Resource),
}
pub type Content = Annotated<RawContent>;
impl RawContent {
impl ContentBlock {
pub fn json<S: Serialize>(json: S) -> Result<Self, crate::ErrorData> {

@@ -179,31 +283,24 @@ let json = serde_json::to_string(&json).map_err(|e| {

})?;
Ok(RawContent::text(json))
Ok(ContentBlock::text(json))
}
pub fn text<S: Into<String>>(text: S) -> Self {
RawContent::Text(RawTextContent {
text: text.into(),
meta: None,
})
pub fn text(text: impl Into<String>) -> Self {
ContentBlock::Text(TextContent::new(text))
}
pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
RawContent::Image(RawImageContent {
data: data.into(),
mime_type: mime_type.into(),
meta: None,
})
pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
ContentBlock::Image(ImageContent::new(data, mime_type))
}
pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
ContentBlock::Audio(AudioContent::new(data, mime_type))
}
pub fn resource(resource: ResourceContents) -> Self {
RawContent::Resource(RawEmbeddedResource {
meta: None,
resource,
})
ContentBlock::Resource(EmbeddedResource::new(resource))
}
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
RawContent::Resource(RawEmbeddedResource {
meta: None,
resource: ResourceContents::TextResourceContents {
pub fn embedded_text(uri: impl Into<String>, content: impl Into<String>) -> Self {
ContentBlock::Resource(EmbeddedResource::new(
ResourceContents::TextResourceContents {
uri: uri.into(),

@@ -214,9 +311,12 @@ mime_type: Some("text".to_string()),

},
})
))
}
/// Get the text content if this is a TextContent variant
pub fn as_text(&self) -> Option<&RawTextContent> {
pub fn resource_link(resource: super::resource::Resource) -> Self {
ContentBlock::ResourceLink(resource)
}
pub fn as_text(&self) -> Option<&TextContent> {
match self {
RawContent::Text(text) => Some(text),
ContentBlock::Text(text) => Some(text),
_ => None,

@@ -226,6 +326,5 @@ }

/// Get the image content if this is an ImageContent variant
pub fn as_image(&self) -> Option<&RawImageContent> {
pub fn as_image(&self) -> Option<&ImageContent> {
match self {
RawContent::Image(image) => Some(image),
ContentBlock::Image(image) => Some(image),
_ => None,

@@ -235,6 +334,5 @@ }

/// Get the resource content if this is an ImageContent variant
pub fn as_resource(&self) -> Option<&RawEmbeddedResource> {
pub fn as_resource(&self) -> Option<&EmbeddedResource> {
match self {
RawContent::Resource(resource) => Some(resource),
ContentBlock::Resource(resource) => Some(resource),
_ => None,

@@ -244,6 +342,5 @@ }

/// Get the resource link if this is a ResourceLink variant
pub fn as_resource_link(&self) -> Option<&super::resource::RawResource> {
pub fn as_resource_link(&self) -> Option<&super::resource::Resource> {
match self {
RawContent::ResourceLink(link) => Some(link),
ContentBlock::ResourceLink(link) => Some(link),
_ => None,

@@ -253,44 +350,28 @@ }

/// Create a resource link content
pub fn resource_link(resource: super::resource::RawResource) -> Self {
RawContent::ResourceLink(resource)
pub fn as_audio(&self) -> Option<&AudioContent> {
match self {
ContentBlock::Audio(audio) => Some(audio),
_ => None,
}
}
}
impl Content {
pub fn text<S: Into<String>>(text: S) -> Self {
RawContent::text(text).no_annotation()
}
// ---------------------------------------------------------------------------
// JsonContent (unchanged)
// ---------------------------------------------------------------------------
pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
RawContent::image(data, mime_type).no_annotation()
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonContent<S: Serialize>(S);
pub fn resource(resource: ResourceContents) -> Self {
RawContent::resource(resource).no_annotation()
}
// ---------------------------------------------------------------------------
// IntoContents
// ---------------------------------------------------------------------------
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
RawContent::embedded_text(uri, content).no_annotation()
}
pub fn json<S: Serialize>(json: S) -> Result<Self, crate::ErrorData> {
RawContent::json(json).map(|c| c.no_annotation())
}
/// Create a resource link content
pub fn resource_link(resource: super::resource::RawResource) -> Self {
RawContent::resource_link(resource).no_annotation()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonContent<S: Serialize>(S);
/// Types that can be converted into a list of contents
/// Types that can be converted into a list of content blocks.
pub trait IntoContents {
fn into_contents(self) -> Vec<Content>;
fn into_contents(self) -> Vec<ContentBlock>;
}
impl IntoContents for Content {
fn into_contents(self) -> Vec<Content> {
impl IntoContents for ContentBlock {
fn into_contents(self) -> Vec<ContentBlock> {
vec![self]

@@ -301,4 +382,4 @@ }

impl IntoContents for String {
fn into_contents(self) -> Vec<Content> {
vec![Content::text(self)]
fn into_contents(self) -> Vec<ContentBlock> {
vec![ContentBlock::text(self)]
}

@@ -308,3 +389,3 @@ }

impl IntoContents for () {
fn into_contents(self) -> Vec<Content> {
fn into_contents(self) -> Vec<ContentBlock> {
vec![]

@@ -322,12 +403,4 @@ }

fn test_image_content_serialization() {
let image_content = RawImageContent {
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
meta: None,
};
let json = serde_json::to_string(&image_content).unwrap();
println!("ImageContent JSON: {}", json);
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
let image = ImageContent::new("base64data", "image/png");
let json = serde_json::to_string(&image).unwrap();
assert!(json.contains("mimeType"));

@@ -339,11 +412,4 @@ assert!(!json.contains("mime_type"));

fn test_audio_content_serialization() {
let audio_content = RawAudioContent {
data: "base64audiodata".to_string(),
mime_type: "audio/wav".to_string(),
};
let json = serde_json::to_string(&audio_content).unwrap();
println!("AudioContent JSON: {}", json);
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
let audio = AudioContent::new("base64audiodata", "audio/wav");
let json = serde_json::to_string(&audio).unwrap();
assert!(json.contains("mimeType"));

@@ -354,6 +420,13 @@ assert!(!json.contains("mime_type"));

#[test]
fn test_audio_content_has_meta() {
let audio = AudioContent::new("data", "audio/wav").with_meta(Meta::default());
let json = serde_json::to_value(&audio).unwrap();
assert!(json.get("_meta").is_some());
}
#[test]
fn test_resource_link_serialization() {
use super::super::resource::RawResource;
use super::super::resource::Resource;
let resource_link = RawContent::ResourceLink(RawResource {
let resource_link = ContentBlock::ResourceLink(Resource {
uri: "file:///test.txt".to_string(),

@@ -367,8 +440,6 @@ name: "test.txt".to_string(),

meta: None,
annotations: None,
});
let json = serde_json::to_string(&resource_link).unwrap();
println!("ResourceLink JSON: {}", json);
// Verify it contains the correct type tag
assert!(json.contains("\"type\":\"resource_link\""));

@@ -389,5 +460,5 @@ assert!(json.contains("\"uri\":\"file:///test.txt\""));

let content: RawContent = serde_json::from_str(json).unwrap();
let content: ContentBlock = serde_json::from_str(json).unwrap();
if let RawContent::ResourceLink(resource) = content {
if let ContentBlock::ResourceLink(resource) = content {
assert_eq!(resource.uri, "file:///example.txt");

@@ -401,2 +472,13 @@ assert_eq!(resource.name, "example.txt");

}
#[test]
fn test_content_block_text_with_annotations() {
let block = ContentBlock::Text(
TextContent::new("hello").with_annotations(Annotations::default().with_priority(0.8)),
);
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "hello");
assert_eq!(json["annotations"]["priority"], 0.8_f32);
}
}

@@ -8,3 +8,3 @@ use std::ops::{Deref, DerefMut};

ClientNotification, ClientRequest, CustomNotification, CustomRequest, Extensions, JsonObject,
JsonRpcMessage, NumberOrString, ProgressToken, ServerNotification, ServerRequest,
JsonRpcMessage, NumberOrString, ProgressToken, ServerNotification, ServerRequest, TaskMetadata,
};

@@ -58,7 +58,7 @@

/// Get a reference to the task field
fn task(&self) -> Option<&JsonObject>;
fn task(&self) -> Option<&TaskMetadata>;
/// Get a mutable reference to the task field
fn task_mut(&mut self) -> &mut Option<JsonObject>;
fn task_mut(&mut self) -> &mut Option<TaskMetadata>;
/// Set the task field
fn set_task(&mut self, task: JsonObject) {
fn set_task(&mut self, task: TaskMetadata) {
*self.task_mut() = Some(task);

@@ -157,5 +157,5 @@ }

CustomRequest
GetTaskInfoRequest
GetTaskRequest
ListTasksRequest
GetTaskResultRequest
GetTaskPayloadRequest
CancelTaskRequest

@@ -170,3 +170,3 @@ }

ListRootsRequest
CreateElicitationRequest
ElicitRequest
CustomRequest

@@ -182,2 +182,3 @@ }

RootsListChangedNotification
TaskStatusNotification
CustomNotification

@@ -196,3 +197,4 @@ }

PromptListChangedNotification
ElicitationCompletionNotification
ElicitationCompleteNotification
TaskStatusNotification
CustomNotification

@@ -199,0 +201,0 @@ }

use serde::{Deserialize, Serialize};
use super::{
AnnotateAble, Annotations, Icon, Meta, RawEmbeddedResource,
content::{EmbeddedResource, ImageContent},
Annotations, ContentBlock, Icon, Meta, Role,
content::{AudioContent, EmbeddedResource, ImageContent, TextContent},
resource::ResourceContents,
};
/// A prompt that can be used to generate text from a model
/// A prompt or prompt template that the server offers (spec `Prompt`).
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]

@@ -15,16 +15,11 @@ #[serde(rename_all = "camelCase")]

pub struct Prompt {
/// The name of the prompt
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// Optional description of what the prompt does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
/// Optional list of icons for the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
/// Optional additional metadata for this prompt
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]

@@ -35,3 +30,2 @@ pub meta: Option<Meta>,

impl Prompt {
/// Create a new prompt with the given name, description and arguments
pub fn new<N, D>(

@@ -56,3 +50,2 @@ name: N,

/// Create a new prompt from raw fields (used by the macro)
pub fn from_raw(

@@ -73,3 +66,2 @@ name: impl Into<String>,

/// Set the human-readable title
pub fn with_title(mut self, title: impl Into<String>) -> Self {

@@ -80,3 +72,2 @@ self.title = Some(title.into());

/// Set the icons
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {

@@ -87,3 +78,2 @@ self.icons = Some(icons);

/// Set the metadata
pub fn with_meta(mut self, meta: Meta) -> Self {

@@ -95,3 +85,3 @@ self.meta = Some(meta);

/// Represents a prompt argument that can be passed to customize the prompt
/// Describes an argument that a prompt can accept (spec `PromptArgument`).
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]

@@ -101,11 +91,7 @@ #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]

pub struct PromptArgument {
/// The name of the argument
pub name: String,
/// A human-readable title for the argument
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// A description of what the argument is used for
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Whether this argument is required
#[serde(skip_serializing_if = "Option::is_none")]

@@ -116,3 +102,2 @@ pub required: Option<bool>,

impl PromptArgument {
/// Create a new prompt argument
pub fn new<N: Into<String>>(name: N) -> Self {

@@ -127,3 +112,2 @@ PromptArgument {

/// Set the title
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {

@@ -134,3 +118,2 @@ self.title = Some(title.into());

/// Set the description
pub fn with_description<D: Into<String>>(mut self, description: D) -> Self {

@@ -141,3 +124,2 @@ self.description = Some(description.into());

/// Set the required flag
pub fn with_required(mut self, required: bool) -> Self {

@@ -149,80 +131,31 @@ self.required = Some(required);

/// Represents the role of a message sender in a prompt conversation
/// A message returned as part of a prompt (spec `PromptMessage`).
///
/// Uses the unified `ContentBlock` for its content (text | image | audio | resource_link | resource).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
pub enum PromptMessageRole {
User,
Assistant,
}
/// Content types that can be included in prompt messages
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
pub enum PromptMessageContent {
/// Plain text content
Text { text: String },
/// Image content with base64-encoded data
Image {
#[serde(flatten)]
image: ImageContent,
},
/// Embedded server-side resource
Resource {
#[serde(flatten)]
resource: EmbeddedResource,
},
/// A link to a resource that can be fetched separately
ResourceLink {
#[serde(flatten)]
link: super::resource::Resource,
},
}
impl PromptMessageContent {
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
/// Create a resource link content
pub fn resource_link(resource: super::resource::Resource) -> Self {
Self::ResourceLink { link: resource }
}
}
/// A message in a prompt conversation
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct PromptMessage {
/// The role of the message sender
pub role: PromptMessageRole,
/// The content of the message
pub content: PromptMessageContent,
pub role: Role,
pub content: ContentBlock,
}
impl PromptMessage {
/// Create a new prompt message with the given role and content
pub fn new(role: PromptMessageRole, content: PromptMessageContent) -> Self {
pub fn new(role: Role, content: ContentBlock) -> Self {
Self { role, content }
}
/// Create a new text message with the given role and text content
pub fn new_text<S: Into<String>>(role: PromptMessageRole, text: S) -> Self {
pub fn new_text<S: Into<String>>(role: Role, text: S) -> Self {
Self {
role,
content: PromptMessageContent::Text { text: text.into() },
content: ContentBlock::text(text),
}
}
/// Create a new image message. `meta` and `annotations` are optional.
#[cfg(feature = "base64")]
pub fn new_image(
role: PromptMessageRole,
role: Role,
data: &[u8],
mime_type: &str,
meta: Option<crate::model::Meta>,
meta: Option<Meta>,
annotations: Option<Annotations>,

@@ -235,21 +168,40 @@ ) -> Self {

role,
content: PromptMessageContent::Image {
image: crate::model::RawImageContent {
data: base64,
mime_type: mime_type.into(),
meta,
}
.optional_annotate(annotations),
},
content: ContentBlock::Image(ImageContent {
data: base64,
mime_type: mime_type.into(),
meta,
annotations,
}),
}
}
/// Create a new resource message. `resource_meta`, `resource_content_meta`, and `annotations` are optional.
#[cfg(feature = "base64")]
pub fn new_audio(
role: Role,
data: &[u8],
mime_type: &str,
meta: Option<Meta>,
annotations: Option<Annotations>,
) -> Self {
use base64::{Engine, prelude::BASE64_STANDARD};
let base64 = BASE64_STANDARD.encode(data);
Self {
role,
content: ContentBlock::Audio(AudioContent {
data: base64,
mime_type: mime_type.into(),
meta,
annotations,
}),
}
}
pub fn new_resource(
role: PromptMessageRole,
role: Role,
uri: String,
mime_type: Option<String>,
text: Option<String>,
resource_meta: Option<crate::model::Meta>,
resource_content_meta: Option<crate::model::Meta>,
resource_meta: Option<Meta>,
resource_content_meta: Option<Meta>,
annotations: Option<Annotations>,

@@ -273,27 +225,25 @@ ) -> Self {

role,
content: PromptMessageContent::Resource {
resource: RawEmbeddedResource {
meta: resource_meta,
resource: resource_contents,
}
.optional_annotate(annotations),
},
content: ContentBlock::Resource(EmbeddedResource {
meta: resource_meta,
resource: resource_contents,
annotations,
}),
}
}
/// Note: PromptMessage text content does not carry protocol-level _meta per current schema.
/// This function exists for API symmetry but ignores the meta parameter.
pub fn new_text_with_meta<S: Into<String>>(
role: PromptMessageRole,
text: S,
_meta: Option<crate::model::Meta>,
) -> Self {
Self::new_text(role, text)
pub fn new_text_with_meta<S: Into<String>>(role: Role, text: S, meta: Option<Meta>) -> Self {
Self {
role,
content: ContentBlock::Text(TextContent {
text: text.into(),
meta,
annotations: None,
}),
}
}
/// Create a new resource link message
pub fn new_resource_link(role: PromptMessageRole, resource: super::resource::Resource) -> Self {
pub fn new_resource_link(role: Role, resource: super::resource::Resource) -> Self {
Self {
role,
content: PromptMessageContent::ResourceLink { link: resource },
content: ContentBlock::ResourceLink(resource),
}

@@ -311,12 +261,4 @@ }

fn test_prompt_message_image_serialization() {
let image_content = crate::model::RawImageContent {
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
meta: None,
};
let json = serde_json::to_string(&image_content).unwrap();
println!("PromptMessage ImageContent JSON: {}", json);
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
let image = ImageContent::new("base64data", "image/png");
let json = serde_json::to_string(&image).unwrap();
assert!(json.contains("mimeType"));

@@ -327,13 +269,43 @@ assert!(!json.contains("mime_type"));

#[test]
fn test_prompt_message_audio_serialization_and_deserialization() {
let content = ContentBlock::Audio(AudioContent::new("YXVkaW8=", "audio/wav"));
let value = serde_json::to_value(&content).unwrap();
assert_eq!(value.get("type").and_then(|v| v.as_str()), Some("audio"));
assert_eq!(value.get("data").and_then(|v| v.as_str()), Some("YXVkaW8="));
assert_eq!(
value.get("mimeType").and_then(|v| v.as_str()),
Some("audio/wav"),
"expected camelCase mimeType, got: {value:#?}"
);
let json = r#"{"type":"audio","data":"YXVkaW8=","mimeType":"audio/wav"}"#;
let parsed: ContentBlock = serde_json::from_str(json).unwrap();
assert_eq!(parsed, content);
}
#[test]
#[cfg(feature = "base64")]
fn test_prompt_message_new_audio_constructor() {
let message = PromptMessage::new_audio(Role::User, b"hello", "audio/wav", None, None);
let value = serde_json::to_value(&message).unwrap();
let content = value.get("content").expect("content present");
assert_eq!(content.get("type").and_then(|v| v.as_str()), Some("audio"));
assert_eq!(
content.get("mimeType").and_then(|v| v.as_str()),
Some("audio/wav")
);
assert_eq!(
content.get("data").and_then(|v| v.as_str()),
Some("aGVsbG8=")
);
}
#[test]
fn test_prompt_message_resource_link_serialization() {
use super::super::resource::RawResource;
use super::super::resource::Resource;
let resource = RawResource::new("file:///test.txt", "test.txt");
let message =
PromptMessage::new_resource_link(PromptMessageRole::User, resource.no_annotation());
let resource = Resource::new("file:///test.txt", "test.txt");
let message = PromptMessage::new_resource_link(Role::User, resource);
let json = serde_json::to_string(&message).unwrap();
println!("PromptMessage with ResourceLink JSON: {}", json);
// Verify it contains the correct type tag
assert!(json.contains("\"type\":\"resource_link\""));

@@ -346,8 +318,4 @@ assert!(json.contains("\"uri\":\"file:///test.txt\""));

fn test_prompt_message_resource_serialization_is_flat() {
// Regression test: PromptMessageContent::Resource must serialize to
// the spec-compliant flat shape `{ "type": "resource", "resource": { "uri", "mimeType", "text" } }`
// and NOT the double-nested shape `{ "type": "resource", "resource": { "resource": {...} } }`.
// See: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts
let message = PromptMessage::new_resource(
PromptMessageRole::User,
Role::User,
"alc://packages/sc/narrative".to_string(),

@@ -362,4 +330,2 @@ Some("text/markdown".to_string()),

let value: serde_json::Value = serde_json::to_value(&message).unwrap();
// Drill into content
let content = value.get("content").expect("content present");

@@ -375,3 +341,2 @@ assert_eq!(

// Spec-compliant: resource.uri / resource.mimeType / resource.text MUST be flat
assert_eq!(

@@ -391,3 +356,2 @@ resource.get("uri").and_then(|v| v.as_str()),

// Regression guard: content.resource MUST NOT contain a nested `resource` key.
assert!(

@@ -409,9 +373,9 @@ resource.get("resource").is_none(),

let content: PromptMessageContent = serde_json::from_str(json).unwrap();
let content: ContentBlock = serde_json::from_str(json).unwrap();
if let PromptMessageContent::ResourceLink { link } = content {
assert_eq!(link.uri, "file:///example.txt");
assert_eq!(link.name, "example.txt");
assert_eq!(link.description, Some("Example file".to_string()));
assert_eq!(link.mime_type, Some("text/plain".to_string()));
if let ContentBlock::ResourceLink(resource) = content {
assert_eq!(resource.uri, "file:///example.txt");
assert_eq!(resource.name, "example.txt");
assert_eq!(resource.description, Some("Example file".to_string()));
assert_eq!(resource.mime_type, Some("text/plain".to_string()));
} else {

@@ -418,0 +382,0 @@ panic!("Expected ResourceLink variant");

use serde::{Deserialize, Serialize};
use super::{Annotated, Icon, Meta};
use super::{Annotations, Icon, Meta};
/// Represents a resource in the extension with metadata
/// A known resource that the server is capable of reading (spec `Resource`).
///
/// Also used as the inner type of `ContentBlock::ResourceLink` (spec `ResourceLink extends Resource`).
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct RawResource {
/// URI representing the resource location (e.g., "file:///path/to/file" or "str:///content")
#[non_exhaustive]
pub struct Resource {
/// The URI of this resource (e.g. `file:///path/to/file`).
pub uri: String,
/// Name of the resource
/// The programmatic name of the resource.
pub name: String,
/// Human-readable title of the resource
/// Optional human-readable display title.
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// Optional description of the resource
/// Optional description of what this resource represents.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// MIME type of the resource content ("text" or "blob")
/// The MIME type of this resource, if known.
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
/// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
///
/// This can be used by Hosts to display file sizes and estimate context window us
/// The size of the raw resource content in bytes (before base64/tokenization), if known.
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u32>,
/// Optional list of icons for the resource
pub size: Option<u64>,
/// Optional set of icons the client may display for this resource.
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
/// Optional additional metadata for this resource
/// Optional protocol-level metadata for this resource.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
/// Optional annotations describing how the client should use this resource.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
pub type Resource = Annotated<RawResource>;
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,
icons: None,
meta: None,
annotations: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
pub fn with_size(mut self, size: u64) -> Self {
self.size = Some(size);
self
}
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
self.icons = Some(icons);
self
}
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
}
/// A template description for resources available on the server (spec `ResourceTemplate`).
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct RawResourceTemplate {
#[non_exhaustive]
pub struct ResourceTemplate {
/// An RFC 6570 URI template for constructing resource URIs.
pub uri_template: String,
/// The programmatic name of the resource template.
pub name: String,
/// Optional human-readable display title.
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// Optional description of what this template is for.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// The MIME type for resources matching this template, if uniform.
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
/// Optional list of icons for the resource template
/// Optional set of icons the client may display for this template.
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
/// Optional protocol-level metadata for this resource template.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
/// Optional annotations describing how the client should use this template.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Annotations>,
}
pub type ResourceTemplate = Annotated<RawResourceTemplate>;
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,
icons: None,
meta: None,
annotations: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
self.icons = Some(icons);
self
}
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
self.annotations = Some(annotations);
self
}
}
/// The contents of a specific resource or sub-resource.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub enum ResourceContents {

@@ -86,3 +193,2 @@ #[serde(rename_all = "camelCase")]

impl ResourceContents {
/// Create text resource contents.
pub fn text(text: impl Into<String>, uri: impl Into<String>) -> Self {

@@ -97,3 +203,2 @@ Self::TextResourceContents {

/// Create blob resource contents.
pub fn blob(blob: impl Into<String>, uri: impl Into<String>) -> Self {

@@ -108,3 +213,2 @@ Self::BlobResourceContents {

/// Set the MIME type on this resource contents.
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {

@@ -118,3 +222,2 @@ match &mut self {

/// Set the metadata on this resource contents.
pub fn with_meta(mut self, meta: Meta) -> Self {

@@ -129,92 +232,2 @@ match &mut self {

impl RawResource {
/// Creates a new Resource from a URI with explicit mime type
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,
icons: None,
meta: None,
}
}
/// Set the human-readable title.
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
/// Set the description.
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
/// Set the MIME type.
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
/// Set the size in bytes.
pub fn with_size(mut self, size: u32) -> Self {
self.size = Some(size);
self
}
/// Set the icons.
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
self.icons = Some(icons);
self
}
/// Set the metadata.
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
}
impl RawResourceTemplate {
/// Creates a new RawResourceTemplate with a URI template and name.
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,
icons: None,
}
}
/// Set the human-readable title.
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
/// Set the description.
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
/// Set the MIME type.
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
/// Set the icons.
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
self.icons = Some(icons);
self
}
}
#[cfg(test)]

@@ -229,17 +242,8 @@ mod tests {

fn test_resource_serialization() {
let resource = RawResource {
uri: "file:///test.txt".to_string(),
title: None,
name: "test".to_string(),
description: Some("Test resource".to_string()),
mime_type: Some("text/plain".to_string()),
size: Some(100),
icons: None,
meta: None,
};
let resource = Resource::new("file:///test.txt", "test")
.with_description("Test resource")
.with_mime_type("text/plain")
.with_size(100);
let json = serde_json::to_string(&resource).unwrap();
println!("Serialized JSON: {}", json);
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
assert!(json.contains("mimeType"));

@@ -259,5 +263,2 @@ assert!(!json.contains("mime_type"));

let json = serde_json::to_string(&text_contents).unwrap();
println!("ResourceContents JSON: {}", json);
// Verify it contains mimeType (camelCase) not mime_type (snake_case)
assert!(json.contains("mimeType"));

@@ -269,9 +270,7 @@ assert!(!json.contains("mime_type"));

fn test_resource_template_with_icons() {
let resource_template = RawResourceTemplate {
uri_template: "file:///{path}".to_string(),
name: "template".to_string(),
title: Some("Test Template".to_string()),
description: Some("A test resource template".to_string()),
mime_type: Some("text/plain".to_string()),
icons: Some(vec![Icon {
let resource_template = ResourceTemplate::new("file:///{path}", "template")
.with_title("Test Template")
.with_description("A test resource template")
.with_mime_type("text/plain")
.with_icons(vec![Icon {
src: "https://example.com/icon.png".to_string(),

@@ -281,4 +280,3 @@ mime_type: Some("image/png".to_string()),

theme: Some(IconTheme::Light),
}]),
};
}]);

@@ -294,14 +292,29 @@ let json = serde_json::to_value(&resource_template).unwrap();

fn test_resource_template_without_icons() {
let resource_template = RawResourceTemplate {
uri_template: "file:///{path}".to_string(),
name: "template".to_string(),
title: None,
description: None,
mime_type: None,
icons: None,
};
let resource_template = ResourceTemplate::new("file:///{path}", "template");
let json = serde_json::to_value(&resource_template).unwrap();
assert!(json.get("icons").is_none());
}
#[test]
fn test_resource_size_u64() {
let resource = Resource::new("file:///big", "big").with_size(5_000_000_000);
let json = serde_json::to_value(&resource).unwrap();
assert_eq!(json["size"], 5_000_000_000_u64);
}
#[test]
fn test_resource_with_annotations() {
let resource = Resource::new("file:///test.txt", "test")
.with_annotations(Annotations::default().with_priority(0.9));
let json = serde_json::to_value(&resource).unwrap();
assert_eq!(json["annotations"]["priority"], 0.9_f32);
}
#[test]
fn test_resource_template_with_meta() {
let resource_template =
ResourceTemplate::new("file:///{path}", "template").with_meta(Meta::default());
let json = serde_json::to_value(&resource_template).unwrap();
assert!(json.get("_meta").is_some());
}
}

@@ -6,2 +6,45 @@ use serde::{Deserialize, Serialize};

/// Metadata for augmenting a request with task execution (spec `TaskMetadata`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct TaskMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<u64>,
}
impl TaskMetadata {
pub fn new() -> Self {
Self::default()
}
pub fn with_ttl(mut self, ttl: u64) -> Self {
self.ttl = Some(ttl);
self
}
}
/// Metadata for associating messages with a task (spec `RelatedTaskMetadata`).
///
/// Carried in `_meta` under the key `"io.modelcontextprotocol/related-task"`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct RelatedTaskMetadata {
pub task_id: String,
}
impl RelatedTaskMetadata {
pub fn new(task_id: impl Into<String>) -> Self {
Self {
task_id: task_id.into(),
}
}
/// The well-known `_meta` key for related-task metadata.
pub const META_KEY: &str = "io.modelcontextprotocol/related-task";
}
/// Canonical task lifecycle status as defined by SEP-1686.

@@ -11,3 +54,3 @@ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub enum TaskStatus {

@@ -100,2 +143,4 @@ /// The receiver accepted the request and is currently working on it.

pub task: Task,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
}

@@ -106,4 +151,10 @@

pub fn new(task: Task) -> Self {
Self { task }
Self { task, meta: None }
}
/// Sets the protocol-level metadata for this result.
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
}

@@ -118,3 +169,3 @@

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct GetTaskResult {

@@ -127,2 +178,8 @@ #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]

impl GetTaskResult {
pub fn new(task: Task) -> Self {
Self { meta: None, task }
}
}
/// Response to a `tasks/result` request.

@@ -171,3 +228,3 @@ ///

#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct CancelTaskResult {

@@ -180,24 +237,6 @@ #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]

/// Paginated list of tasks
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct TaskList {
pub tasks: Vec<Task>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<u64>,
}
impl TaskList {
/// Create a new TaskList.
pub fn new(tasks: Vec<Task>) -> Self {
Self {
tasks,
next_cursor: None,
total: None,
}
impl CancelTaskResult {
pub fn new(task: Task) -> Self {
Self { meta: None, task }
}
}

@@ -403,4 +403,5 @@ use futures::FutureExt;

params: CancelledNotificationParam {
request_id: self.id.clone(),
request_id: Some(self.id.clone()),
reason: Some(reason.to_owned()),
meta: None,
},

@@ -477,4 +478,5 @@ method: crate::model::CancelledNotificationMethod,

params: CancelledNotificationParam {
request_id: self.id,
request_id: Some(self.id),
reason,
meta: None,
},

@@ -1089,7 +1091,9 @@ method: crate::model::CancelledNotificationMethod,

if let Some(param) = cancellation_param {
if let Some(responder) = local_responder_pool.remove(&param.request_id) {
tracing::info!(id = %param.request_id, reason = param.reason, "cancelled");
let _response_result = responder.send(Err(ServiceError::Cancelled {
reason: param.reason.clone(),
}));
if let Some(request_id) = &param.request_id {
if let Some(responder) = local_responder_pool.remove(request_id) {
tracing::info!(id = %request_id, reason = param.reason, "cancelled");
let _response_result = responder.send(Err(ServiceError::Cancelled {
reason: param.reason.clone(),
}));
}
}

@@ -1207,5 +1211,7 @@ }

Ok::<CancelledNotification, _>(cancelled) => {
if let Some(ct) = local_ct_pool.remove(&cancelled.params.request_id) {
tracing::info!(id = %cancelled.params.request_id, reason = cancelled.params.reason, "cancelled");
ct.cancel();
if let Some(request_id) = &cancelled.params.request_id {
if let Some(ct) = local_ct_pool.remove(request_id) {
tracing::info!(id = %request_id, reason = cancelled.params.reason, "cancelled");
ct.cancel();
}
}

@@ -1212,0 +1218,0 @@ cancelled.into()

@@ -0,1 +1,3 @@

// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
#![expect(deprecated)]
use std::borrow::Cow;

@@ -2,0 +4,0 @@

@@ -0,1 +1,3 @@

// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
#![expect(deprecated)]
use std::borrow::Cow;

@@ -12,4 +14,4 @@ #[cfg(feature = "elicitation")]

use crate::model::{
CreateElicitationRequest, CreateElicitationRequestParams, CreateElicitationResult,
ElicitationAction, ElicitationCompletionNotification, ElicitationResponseNotificationParam,
ElicitRequest, ElicitRequestParams, ElicitResult, ElicitationAction,
ElicitationCompleteNotification, ElicitationResponseNotificationParam,
};

@@ -468,7 +470,7 @@ use crate::{

#[cfg(feature = "elicitation")]
method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult);
method!(peer_req create_elicitation ElicitRequest(ElicitRequestParams) => ElicitResult);
#[cfg(feature = "elicitation")]
method!(peer_req_with_timeout create_elicitation_with_timeout CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult);
method!(peer_req_with_timeout create_elicitation_with_timeout ElicitRequest(ElicitRequestParams) => ElicitResult);
#[cfg(feature = "elicitation")]
method!(peer_not notify_url_elicitation_completed ElicitationCompletionNotification(ElicitationResponseNotificationParam));
method!(peer_not notify_url_elicitation_completed ElicitationCompleteNotification(ElicitationResponseNotificationParam));

@@ -792,3 +794,3 @@ method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam));

.create_elicitation_with_timeout(
CreateElicitationRequestParams::FormElicitationParams {
ElicitRequestParams::FormElicitationParams {
meta: None,

@@ -926,3 +928,3 @@ message: message.into(),

.create_elicitation_with_timeout(
CreateElicitationRequestParams::UrlElicitationParams {
ElicitRequestParams::UrlElicitationParams {
meta: None,

@@ -929,0 +931,0 @@ message: message.into(),

@@ -312,2 +312,4 @@ use std::{borrow::Cow, collections::HashMap, sync::Arc};

mod tests {
use rstest::rstest;
use super::parse_json_rpc_error;

@@ -350,23 +352,13 @@ use crate::{

#[test]
fn parse_json_rpc_error_rejects_non_error_request() {
// A valid JSON-RPC request (method + id) must not be accepted as an error.
let body = r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#;
#[rstest]
#[case::non_error_request(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#)]
#[case::notification(
r#"{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":1}}"#
)]
#[case::plain_text("not json at all")]
#[case::empty("")]
#[case::truncated_json(r#"{"broken":"#)]
fn parse_json_rpc_error_rejects_non_error_bodies(#[case] body: &str) {
assert!(parse_json_rpc_error(body).is_none());
}
#[test]
fn parse_json_rpc_error_rejects_notification() {
// A notification (method, no id) must not be accepted as an error.
let body =
r#"{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":1}}"#;
assert!(parse_json_rpc_error(body).is_none());
}
#[test]
fn parse_json_rpc_error_rejects_malformed_json() {
assert!(parse_json_rpc_error("not json at all").is_none());
assert!(parse_json_rpc_error("").is_none());
assert!(parse_json_rpc_error(r#"{"broken":"#).is_none());
}
}

@@ -432,5 +432,6 @@ use std::{

if let ClientNotification::CancelledNotification(n) = &notification.notification {
let request_id = n.params.request_id.clone();
let resource = ResourceKey::McpRequestId(request_id);
self.unregister_resource(&resource);
if let Some(request_id) = n.params.request_id.clone() {
let resource = ResourceKey::McpRequestId(request_id);
self.unregister_resource(&resource);
}
}

@@ -500,9 +501,13 @@ }

}) => {
if let Some(id) = self
.resource_router
.get(&ResourceKey::McpRequestId(request_id.clone()))
{
OutboundChannel::RequestWise {
id: *id,
close: false,
if let Some(req_id) = request_id {
if let Some(id) = self
.resource_router
.get(&ResourceKey::McpRequestId(req_id.clone()))
{
OutboundChannel::RequestWise {
id: *id,
close: false,
}
} else {
OutboundChannel::Common
}

@@ -509,0 +514,0 @@ } else {

@@ -1127,40 +1127,36 @@ use std::{

} else {
let (session_id, transport) = self
.session_manager
.create_session()
.await
.map_err(internal_error_response("create session"))?;
// Capture init params for external store persistence before
// extensions are injected (which would require Clone).
let stored_init_params = if self.config.session_store.is_some() {
if let ClientJsonRpcMessage::Request(req) = &message {
if let ClientRequest::InitializeRequest(init_req) = &req.request {
Some(init_req.params.clone())
} else {
None
}
} else {
None
let stored_init_params = match &mut message {
ClientJsonRpcMessage::Request(req) => {
let ClientRequest::InitializeRequest(init_req) = &req.request else {
return Err(unexpected_message_response("initialize request"));
};
// Reject mismatched MCP-Protocol-Version header before binding the session to anything.
validate_header_matches_init_body(
&part.headers,
init_req.params.protocol_version.as_str(),
Some(req.id.clone()),
)?;
let stored_init_params = self
.config
.session_store
.as_ref()
.map(|_| init_req.params.clone());
// inject request part to extensions
req.request.extensions_mut().insert(part);
stored_init_params
}
} else {
None
_ => {
return Err(unexpected_message_response("initialize request"));
}
};
if let ClientJsonRpcMessage::Request(req) = &mut message {
let ClientRequest::InitializeRequest(init_req) = &req.request else {
return Err(unexpected_message_response("initialize request"));
};
// Reject mismatched MCP-Protocol-Version header before binding the session to anything.
validate_header_matches_init_body(
&part.headers,
init_req.params.protocol_version.as_str(),
Some(req.id.clone()),
)?;
// inject request part to extensions
req.request.extensions_mut().insert(part);
} else {
return Err(unexpected_message_response("initialize request"));
}
let service = self
.get_service()
.map_err(internal_error_response("get service"))?;
let (session_id, transport) = self
.session_manager
.create_session()
.await
.map_err(internal_error_response("create session"))?;
// spawn a task to serve the session

@@ -1167,0 +1163,0 @@ Self::spawn_session_worker(

@@ -0,1 +1,3 @@

// Sampling/Roots/Logging are SEP-2577-deprecated; this test handler exercises them.
#![expect(deprecated)]
use std::{

@@ -174,6 +176,8 @@ future::Future,

if let Err(e) = peer
.notify_logging_message(LoggingMessageNotificationParam {
level: request.level,
data,
logger,
.notify_logging_message({
let mut param = LoggingMessageNotificationParam::new(request.level, data);
if let Some(l) = logger {
param = param.with_logger(l);
}
param
})

@@ -180,0 +184,0 @@ .await

@@ -57,6 +57,3 @@ use std::collections::HashMap;

Reference::for_prompt("weather_prompt"),
ArgumentInfo {
name: "location".to_string(),
value: "San".to_string(),
},
ArgumentInfo::new("location", "San"),
)

@@ -148,7 +145,4 @@ .with_context(CompletionContext::with_arguments(args));

// Test that completion follows MCP 2025-06-18 specification format
let completion = CompletionInfo {
values: vec!["value1".to_string(), "value2".to_string()],
total: Some(2),
has_more: Some(false),
};
let completion =
CompletionInfo::with_all_values(vec!["value1".to_string(), "value2".to_string()]).unwrap();

@@ -167,14 +161,14 @@ let json = serde_json::to_value(&completion).unwrap();

fn test_resource_reference() {
// Test that ResourceReference works correctly
let resource_ref = ResourceReference {
uri: "test://uri".to_string(),
};
// ResourceTemplateReference가 `ref/resource` 와이어 태그로 직렬화/역직렬화되는지 확인
let reference = Reference::for_resource("test://uri");
// Test that ResourceReference works correctly
let another_ref = ResourceReference {
uri: "test://uri".to_string(),
};
let json = serde_json::to_value(&reference).unwrap();
assert_eq!(json["type"], "ref/resource");
assert_eq!(json["uri"], "test://uri");
// They should be equivalent
assert_eq!(resource_ref.uri, another_ref.uri);
let back: Reference = serde_json::from_value(json).unwrap();
match back {
Reference::Resource(r) => assert_eq!(r.uri, "test://uri"),
other => panic!("expected Reference::Resource, got {other:?}"),
}
}

@@ -203,6 +197,3 @@

Reference::for_resource("file://{path}"),
ArgumentInfo {
name: "path".to_string(),
value: "src/".to_string(),
},
ArgumentInfo::new("path", "src/"),
);

@@ -209,0 +200,0 @@

@@ -43,3 +43,3 @@ #![allow(clippy::exhaustive_structs, clippy::exhaustive_enums)]

) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request.0)?;
let content = ContentBlock::json(chat_request.0)?;
Ok(CallToolResult::success(vec![content]))

@@ -46,0 +46,0 @@ }

@@ -129,3 +129,4 @@ use rmcp::model::{JsonRpcResponse, ServerJsonRpcMessage, ServerResult};

fn round_trip_call_tool_result_preserves_variant() {
let original = CallToolResult::success(vec![rmcp::model::Content::text("hello world")]);
let original =
CallToolResult::success(vec![rmcp::model::ContentBlock::text("hello world")]);
let json = serde_json::to_value(&original).unwrap();

@@ -132,0 +133,0 @@ let result = parse_result(wrap_response(json));

@@ -1,2 +0,2 @@

use rmcp::model::{AnnotateAble, Content, Meta, RawContent, ResourceContents};
use rmcp::model::{ContentBlock, EmbeddedResource, Meta, ResourceContents};
use serde_json::json;

@@ -6,13 +6,10 @@

fn serialize_embedded_text_resource_with_meta() {
// Inner contents meta
let mut resource_content_meta = Meta::new();
resource_content_meta.insert("inner".to_string(), json!(2));
// Top-level embedded resource meta
let mut resource_meta = Meta::new();
resource_meta.insert("top".to_string(), json!(1));
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
meta: Some(resource_meta),
resource: ResourceContents::TextResourceContents {
let content = ContentBlock::Resource(
EmbeddedResource::new(ResourceContents::TextResourceContents {
uri: "str://example".to_string(),

@@ -22,5 +19,5 @@ mime_type: Some("text/plain".to_string()),

meta: Some(resource_content_meta),
},
})
.no_annotation();
})
.with_meta(resource_meta),
);

@@ -45,5 +42,4 @@ let v = serde_json::to_value(&content).unwrap();

fn serialize_embedded_text_resource_without_meta_omits_fields() {
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
meta: None,
resource: ResourceContents::TextResourceContents {
let content = ContentBlock::Resource(EmbeddedResource::new(
ResourceContents::TextResourceContents {
uri: "str://no-meta".to_string(),

@@ -54,4 +50,3 @@ mime_type: Some("text/plain".to_string()),

},
})
.no_annotation();
));

@@ -77,15 +72,13 @@ let v = serde_json::to_value(&content).unwrap();

let content: Content = serde_json::from_value(raw).unwrap();
let content: ContentBlock = serde_json::from_value(raw).unwrap();
let raw = match &content.raw {
RawContent::Resource(er) => er,
let er = match &content {
ContentBlock::Resource(er) => er,
_ => panic!("expected resource"),
};
// top-level _meta
let top = raw.meta.as_ref().expect("top-level meta missing");
let top = er.meta.as_ref().expect("top-level meta missing");
assert_eq!(top.get("x").unwrap(), &json!(true));
// inner contents _meta
match &raw.resource {
match &er.resource {
ResourceContents::TextResourceContents {

@@ -111,5 +104,4 @@ meta, uri, text, ..

let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
meta: Some(resource_meta),
resource: ResourceContents::BlobResourceContents {
let content = ContentBlock::Resource(
EmbeddedResource::new(ResourceContents::BlobResourceContents {
uri: "str://blob".to_string(),

@@ -119,5 +111,5 @@ mime_type: Some("application/octet-stream".to_string()),

meta: Some(resource_content_meta),
},
})
.no_annotation();
})
.with_meta(resource_meta),
);

@@ -124,0 +116,0 @@ let v = serde_json::to_value(&content).unwrap();

@@ -26,2 +26,3 @@ #![cfg(all(feature = "client", feature = "server", not(feature = "local")))]

struct SlowToolServer {
#[expect(dead_code, reason = "tool_handler macro accesses this router field")]
tool_router: ToolRouter<Self>,

@@ -152,3 +153,3 @@ }

.first()
.and_then(|c| c.raw.as_text())
.and_then(|c| c.as_text())
.map(|t| t.text.as_str())

@@ -155,0 +156,0 @@ .expect("expected text content in tool result");

@@ -29,10 +29,12 @@ // cargo test --features "server client" --package rmcp test_logging

.peer()
.notify_logging_message(LoggingMessageNotificationParam {
level: LoggingLevel::Info,
data: serde_json::json!({
"message": "Server initiated message",
"timestamp": chrono::Utc::now().to_rfc3339(),
}),
logger: Some("test_server".to_string()),
})
.notify_logging_message(
LoggingMessageNotificationParam::new(
LoggingLevel::Info,
serde_json::json!({
"message": "Server initiated message",
"timestamp": chrono::Utc::now().to_rfc3339(),
}),
)
.with_logger("test_server"),
)
.await?;

@@ -281,10 +283,5 @@

for (level, has_logger) in [(LoggingLevel::Info, true), (LoggingLevel::Debug, false)] {
server
.peer()
.notify_logging_message(LoggingMessageNotificationParam {
level,
data: json!({"test": "data"}),
logger: has_logger.then(|| "test_logger".to_string()),
})
.await?;
let mut param = LoggingMessageNotificationParam::new(level, json!({"test": "data"}));
param.logger = has_logger.then(|| "test_logger".to_string());
server.peer().notify_logging_message(param).await?;
}

@@ -291,0 +288,0 @@

//cargo test --test test_message_protocol --features "client server"
#![cfg(not(feature = "local"))]
#![expect(deprecated)] // exercises SEP-2577-deprecated Sampling/Roots/Logging types

@@ -4,0 +5,0 @@ mod common;

@@ -15,3 +15,3 @@ #![cfg(not(feature = "local"))]

pub struct Server {}
struct Server {}

@@ -42,3 +42,3 @@ impl ServerHandler for Server {

if let Err(e) = peer
.notify_resource_updated(ResourceUpdatedNotificationParam { uri: uri.clone() })
.notify_resource_updated(ResourceUpdatedNotificationParam::new(uri.clone()))
.await

@@ -45,0 +45,0 @@ {

@@ -42,2 +42,3 @@ #![cfg(not(feature = "local"))]

pub struct MyServer {
#[expect(dead_code, reason = "tool_handler macro accesses this router field")]
tool_router: ToolRouter<Self>,

@@ -75,8 +76,7 @@ }

let _ = client
.notify_progress(ProgressNotificationParam {
progress_token: progress_token.clone(),
progress: (step as f64),
total: Some(10.0),
message: Some("Some message".into()),
})
.notify_progress(
ProgressNotificationParam::new(progress_token.clone(), step as f64)
.with_total(10.0)
.with_message("Some message"),
)
.await;

@@ -83,0 +83,0 @@ tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

@@ -6,9 +6,3 @@ //cargo test --test test_prompt_handler --features "client server"

use rmcp::{
RoleServer, ServerHandler,
handler::server::router::prompt::PromptRouter,
model::{GetPromptRequestParams, GetPromptResult, ListPromptsResult, PaginatedRequestParams},
prompt_handler,
service::RequestContext,
};
use rmcp::{ServerHandler, handler::server::router::prompt::PromptRouter, prompt_handler};

@@ -15,0 +9,0 @@ #[derive(Debug, Clone)]

@@ -7,3 +7,3 @@ //cargo test --test test_prompt_macro_annotations --features "client server"

handler::server::wrapper::Parameters,
model::{GetPromptResult, Prompt, PromptMessage, PromptMessageRole},
model::{GetPromptResult, Prompt, PromptMessage, Role},
prompt,

@@ -44,6 +44,3 @@ };

async fn basic_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Basic response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Basic response")]
}

@@ -54,6 +51,3 @@

async fn named_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Named response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Named response")]
}

@@ -65,3 +59,3 @@

vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Described response",

@@ -75,3 +69,3 @@ )]

vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Fully custom response",

@@ -87,3 +81,3 @@ )]

vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Doc comment response",

@@ -98,3 +92,3 @@ )]

vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Override response",

@@ -107,6 +101,3 @@ )]

async fn args_prompt(_server: &TestServer, _args: Parameters<TestArgs>) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Args response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Args response")]
}

@@ -121,3 +112,3 @@

GetPromptResult::new(vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Complex response",

@@ -131,6 +122,3 @@ )])

fn sync_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Sync response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Sync response")]
}

@@ -288,6 +276,3 @@

) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Generic response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Generic response")]
}

@@ -294,0 +279,0 @@

@@ -10,4 +10,4 @@ #![cfg(not(feature = "local"))]

model::{
ClientInfo, GetPromptRequestParams, GetPromptResult, ListPromptsResult,
PaginatedRequestParams, PromptMessage, PromptMessageRole,
ClientInfo, ContentBlock, GetPromptRequestParams, GetPromptResult, ListPromptsResult,
PaginatedRequestParams, PromptMessage, Role,
},

@@ -21,5 +21,5 @@ prompt, prompt_handler, prompt_router,

#[derive(Serialize, Deserialize, JsonSchema)]
pub struct CodeReviewRequest {
pub file_path: String,
pub language: String,
struct CodeReviewRequest {
file_path: String,
language: String,
}

@@ -58,3 +58,3 @@

PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!(

@@ -66,3 +66,3 @@ "Please review the {} code in: {}",

PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"I'll review this code for best practices and potential issues.".to_string(),

@@ -76,3 +76,3 @@ ),

vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"This is a prompt with no parameters.".to_string(),

@@ -118,7 +118,7 @@ )]

PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
"I need help with the current context.".to_string(),
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(

@@ -150,4 +150,4 @@ "Based on the context '{}', here's how I can help...",

assert_eq!(result.len(), 2);
assert_eq!(result[0].role, PromptMessageRole::User);
assert_eq!(result[1].role, PromptMessageRole::Assistant);
assert_eq!(result[0].role, Role::User);
assert_eq!(result[1].role, Role::Assistant);
}

@@ -176,4 +176,4 @@

match &result.messages[1].content {
rmcp::model::PromptMessageContent::Text { text } => {
assert!(text.contains("mock context data"));
ContentBlock::Text(text_content) => {
assert!(text_content.text.contains("mock context data"));
}

@@ -205,5 +205,5 @@ _ => panic!("Expected text content"),

#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct OptionalFieldTestSchema {
struct OptionalFieldTestSchema {
#[schemars(description = "An optional description field")]
pub description: Option<String>,
description: Option<String>,
}

@@ -213,6 +213,6 @@

#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct OptionalI64TestSchema {
struct OptionalI64TestSchema {
#[schemars(description = "An optional i64 field")]
pub count: Option<i64>,
pub mandatory_field: String, // Added to ensure non-empty object schema
count: Option<i64>,
mandatory_field: String, // Added to ensure non-empty object schema
}

@@ -246,3 +246,3 @@

vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Testing optional fields".to_string(),

@@ -263,7 +263,4 @@ )]

GetPromptResult::new(vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
message,
)])
.with_description("Test result for optional i64")
GetPromptResult::new(vec![PromptMessage::new_text(Role::Assistant, message)])
.with_description("Test result for optional i64")
}

@@ -353,3 +350,3 @@ }

let result_text = match &result.messages.first().unwrap().content {
rmcp::model::PromptMessageContent::Text { text } => text.as_str(),
ContentBlock::Text(text_content) => text_content.text.as_str(),
_ => panic!("Expected text content"),

@@ -379,3 +376,3 @@ };

let some_result_text = match &some_result.messages.first().unwrap().content {
rmcp::model::PromptMessageContent::Text { text } => text.as_str(),
ContentBlock::Text(text_content) => text_content.text.as_str(),
_ => panic!("Expected text content"),

@@ -382,0 +379,0 @@ };

@@ -8,8 +8,8 @@ #![cfg(not(feature = "local"))]

handler::server::wrapper::Parameters,
model::{GetPromptResult, PromptMessage, PromptMessageRole},
model::{GetPromptResult, PromptMessage, Role},
};
#[derive(Debug, Default)]
pub struct TestHandler<T: 'static = ()> {
pub _marker: std::marker::PhantomData<fn(*const T)>,
struct TestHandler<T: 'static = ()> {
_marker: std::marker::PhantomData<fn(*const T)>,
}

@@ -20,10 +20,10 @@

#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub fields: HashMap<String, String>,
struct Request {
fields: HashMap<String, String>,
}
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
pub struct Sum {
pub a: i32,
pub b: i32,
struct Sum {
a: i32,
b: i32,
}

@@ -40,3 +40,3 @@

vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Async method response",

@@ -53,3 +53,3 @@ )]

vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Sync method response",

@@ -64,3 +64,3 @@ )]

vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Async function response",

@@ -74,3 +74,3 @@ )]

GetPromptResult::new(vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Async function 2 response",

@@ -77,0 +77,0 @@ )])

@@ -62,8 +62,7 @@ #![cfg(not(feature = "local"))]

let _ = client
.notify_progress(ProgressNotificationParam {
progress_token: progress_token.clone(),
progress: step as f64,
total: Some(4.0),
message: Some("working".into()),
})
.notify_progress(
ProgressNotificationParam::new(progress_token.clone(), step as f64)
.with_total(4.0)
.with_message("working"),
)
.await;

@@ -83,8 +82,10 @@ }

let _ = client
.notify_progress(ProgressNotificationParam {
progress_token: ProgressToken(NumberOrString::Number(999_999)),
progress: step as f64,
total: Some(4.0),
message: Some("unrelated".into()),
})
.notify_progress(
ProgressNotificationParam::new(
ProgressToken(NumberOrString::Number(999_999)),
step as f64,
)
.with_total(4.0)
.with_message("unrelated"),
)
.await;

@@ -91,0 +92,0 @@ }

/// Integration tests for resource_link support in both tools and prompts
use rmcp::model::{
AnnotateAble, CallToolResult, Content, PromptMessage, PromptMessageContent, PromptMessageRole,
RawResource, Resource,
};
use rmcp::model::{CallToolResult, ContentBlock, PromptMessage, Resource, Role};
#[test]
fn test_tool_and_prompt_resource_link_compatibility() {
// Create a resource that can be used in both tools and prompts
let resource = RawResource::new("file:///shared/data.json", "Shared Data");
let resource_annotated: Resource = resource.clone().no_annotation();
let resource = Resource::new("file:///shared/data.json", "Shared Data");
// Test 1: Tool returning a resource link
let tool_result = CallToolResult::success(vec![
Content::text("Found shared data"),
Content::resource_link(resource.clone()),
ContentBlock::text("Found shared data"),
ContentBlock::resource_link(resource.clone()),
]);

@@ -23,4 +18,3 @@

// Test 2: Prompt returning a resource link
let prompt_message =
PromptMessage::new_resource_link(PromptMessageRole::Assistant, resource_annotated.clone());
let prompt_message = PromptMessage::new_resource_link(Role::Assistant, resource.clone());

@@ -34,7 +28,5 @@ let prompt_json = serde_json::to_string(&prompt_message).unwrap();

// Extract just the resource link parts
let tool_resource_json = serde_json::to_value(tool_content).unwrap();
let prompt_resource_json = serde_json::to_value(prompt_content).unwrap();
// Both should have the same structure
assert_eq!(

@@ -56,12 +48,9 @@ tool_resource_json.get("type").unwrap(),

fn test_resource_link_roundtrip() {
// Test that resource links can be serialized and deserialized correctly
// in both tool results and prompt messages
let resource = Resource::new("https://api.example.com/resource", "API Resource")
.with_description("External API resource")
.with_mime_type("application/json")
.with_size(2048);
let mut resource = RawResource::new("https://api.example.com/resource", "API Resource");
resource.description = Some("External API resource".to_string());
resource.mime_type = Some("application/json".to_string());
resource.size = Some(2048);
// Test with tool result
let tool_result = CallToolResult::success(vec![Content::resource_link(resource.clone())]);
let tool_result = CallToolResult::success(vec![ContentBlock::resource_link(resource.clone())]);

@@ -88,6 +77,3 @@ let tool_json = serde_json::to_string(&tool_result).unwrap();

// Test with prompt message
let prompt_message = PromptMessage::new(
PromptMessageRole::User,
PromptMessageContent::resource_link(resource.no_annotation()),
);
let prompt_message = PromptMessage::new(Role::User, ContentBlock::resource_link(resource));

@@ -97,3 +83,3 @@ let prompt_json = serde_json::to_string(&prompt_message).unwrap();

if let PromptMessageContent::ResourceLink { link } = prompt_deserialized.content {
if let ContentBlock::ResourceLink(link) = &prompt_deserialized.content {
assert_eq!(link.uri, "https://api.example.com/resource");

@@ -111,14 +97,11 @@ assert_eq!(link.name, "API Resource");

fn test_mixed_content_in_prompts_and_tools() {
// Test that resource links can be mixed with other content types
// in both prompts and tools
let resource1 = Resource::new("file:///doc1.md", "Document 1");
let resource2 = Resource::new("file:///doc2.md", "Document 2");
let resource1 = RawResource::new("file:///doc1.md", "Document 1");
let resource2 = RawResource::new("file:///doc2.md", "Document 2");
// Tool with mixed content
let tool_result = CallToolResult::success(vec![
Content::text("Processing complete. Found documents:"),
Content::resource_link(resource1.clone()),
Content::resource_link(resource2.clone()),
Content::embedded_text("summary://result", "Both documents processed successfully"),
ContentBlock::text("Processing complete. Found documents:"),
ContentBlock::resource_link(resource1),
ContentBlock::resource_link(resource2),
ContentBlock::embedded_text("summary://result", "Both documents processed successfully"),
]);

@@ -125,0 +108,0 @@

@@ -1,2 +0,2 @@

use rmcp::model::{CallToolResult, Content, RawResource};
use rmcp::model::{CallToolResult, ContentBlock, Resource};

@@ -6,8 +6,8 @@ #[test]

// Test creating a tool result with resource links
let resource = RawResource::new("file:///test/file.txt", "test.txt");
let resource = Resource::new("file:///test/file.txt", "test.txt");
// Create a tool result with a resource link
let result = CallToolResult::success(vec![
Content::text("Found a file"),
Content::resource_link(resource),
ContentBlock::text("Found a file"),
ContentBlock::resource_link(resource),
]);

@@ -46,8 +46,8 @@

fn test_resource_link_with_full_metadata() {
let mut resource = RawResource::new("https://example.com/data.json", "API Data");
resource.description = Some("JSON data from external API".to_string());
resource.mime_type = Some("application/json".to_string());
resource.size = Some(1024);
let resource = Resource::new("https://example.com/data.json", "API Data")
.with_description("JSON data from external API")
.with_mime_type("application/json")
.with_size(1024);
let result = CallToolResult::success(vec![Content::resource_link(resource)]);
let result = CallToolResult::success(vec![ContentBlock::resource_link(resource)]);

@@ -76,8 +76,8 @@ let json = serde_json::to_string(&result).unwrap();

// Test that resource links can be mixed with other content types
let resource = RawResource::new("file:///doc.pdf", "Document");
let resource = Resource::new("file:///doc.pdf", "Document");
let result = CallToolResult::success(vec![
Content::text("Processing complete"),
Content::resource_link(resource),
Content::embedded_text("memo://result", "Analysis results here"),
ContentBlock::text("Processing complete"),
ContentBlock::resource_link(resource),
ContentBlock::embedded_text("memo://result", "Analysis results here"),
]);

@@ -84,0 +84,0 @@

@@ -348,3 +348,3 @@ #![cfg(not(feature = "local"))]

"call_123",
vec![Content::text(
vec![ContentBlock::text(
"The weather in San Francisco is 72°F and sunny.",

@@ -363,2 +363,13 @@ )],

#[test]
fn test_tool_result_content_requires_content() {
let raw = serde_json::json!({
"toolUseId": "call_123"
});
let err = serde_json::from_value::<ToolResultContent>(raw).unwrap_err();
assert!(err.to_string().contains("missing field `content`"));
}
#[tokio::test]

@@ -391,3 +402,3 @@ async fn test_sampling_message_with_tool_use() -> Result<()> {

let message =
SamplingMessage::user_tool_result("call_123", vec![Content::text("72°F and sunny")]);
SamplingMessage::user_tool_result("call_123", vec![ContentBlock::text("72°F and sunny")]);

@@ -437,6 +448,4 @@ let json = serde_json::to_string(&message)?;

async fn test_sampling_capability() -> Result<()> {
let cap = SamplingCapability {
tools: Some(JsonObject::default()),
context: None,
};
let mut cap = SamplingCapability::default();
cap.tools = Some(JsonObject::default());

@@ -514,4 +523,4 @@ let json = serde_json::to_string(&cap)?;

let content = Content::text("Hello");
let sampling_content: SamplingMessageContent =
let content = ContentBlock::text("Hello");
let sampling_content: SamplingMessageContentBlock =
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;

@@ -521,6 +530,9 @@ assert!(sampling_content.as_text().is_some());

let content = Content::image("base64data", "image/png");
let sampling_content: SamplingMessageContent =
let content = ContentBlock::image("base64data", "image/png");
let sampling_content: SamplingMessageContentBlock =
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
assert!(matches!(sampling_content, SamplingMessageContent::Image(_)));
assert!(matches!(
sampling_content,
SamplingMessageContentBlock::Image(_)
));

@@ -534,4 +546,4 @@ Ok(())

let content = Content::text("Hello");
let sampling_content: SamplingContent<SamplingMessageContent> =
let content = ContentBlock::text("Hello");
let sampling_content: SamplingContent<SamplingMessageContentBlock> =
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;

@@ -550,3 +562,3 @@ assert_eq!(sampling_content.len(), 1);

let resource_content = Content::resource(ResourceContents::TextResourceContents {
let resource_content = ContentBlock::resource(ResourceContents::TextResourceContents {
uri: "file:///test.txt".to_string(),

@@ -558,3 +570,3 @@ mime_type: Some("text/plain".to_string()),

let result: Result<SamplingMessageContent, _> = resource_content.try_into();
let result: Result<SamplingMessageContentBlock, _> = resource_content.try_into();
assert!(result.is_err());

@@ -572,3 +584,3 @@ assert_eq!(

Role::User,
SamplingMessageContent::tool_use("call_1", "some_tool", Default::default()),
SamplingMessageContentBlock::tool_use("call_1", "some_tool", Default::default()),
)],

@@ -590,3 +602,3 @@ 100,

Role::Assistant,
SamplingMessageContent::tool_result("call_1", vec![Content::text("result")]),
SamplingMessageContentBlock::tool_result("call_1", vec![ContentBlock::text("result")]),
)],

@@ -609,4 +621,7 @@ 100,

vec![
SamplingMessageContent::tool_result("call_1", vec![Content::text("result")]),
SamplingMessageContent::text("some extra text"),
SamplingMessageContentBlock::tool_result(
"call_1",
vec![ContentBlock::text("result")],
),
SamplingMessageContentBlock::text("some extra text"),
],

@@ -646,3 +661,6 @@ )],

SamplingMessage::user_text("Hello"),
SamplingMessage::user_tool_result("nonexistent_call", vec![Content::text("result")]),
SamplingMessage::user_tool_result(
"nonexistent_call",
vec![ContentBlock::text("result")],
),
],

@@ -672,3 +690,3 @@ 100,

),
SamplingMessage::user_tool_result("call_1", vec![Content::text("72°F and sunny")]),
SamplingMessage::user_tool_result("call_1", vec![ContentBlock::text("72°F and sunny")]),
SamplingMessage::assistant_text("It's 72°F and sunny in SF."),

@@ -675,0 +693,0 @@ ],

@@ -51,4 +51,6 @@ #![cfg(not(feature = "local"))]

ServerCapabilities::builder()
.enable_tools_with(ToolsCapability {
list_changed: Some(true),
.enable_tools_with({
let mut tools = ToolsCapability::default();
tools.list_changed = Some(true);
tools
})

@@ -55,0 +57,0 @@ .build(),

#![cfg(not(feature = "local"))]
//! Regression tests for the `MCP-Protocol-Version` header / initialize body consistency check.
use std::sync::Arc;
use rmcp::transport::streamable_http_server::{

@@ -20,5 +22,12 @@ StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,

) -> (reqwest::Client, String, CancellationToken) {
spawn_server_with_manager(config, Arc::new(LocalSessionManager::default())).await
}
async fn spawn_server_with_manager(
config: StreamableHttpServerConfig,
session_manager: Arc<LocalSessionManager>,
) -> (reqwest::Client, String, CancellationToken) {
let ct = config.cancellation_token.clone();
let service: StreamableHttpService<Calculator, LocalSessionManager> =
StreamableHttpService::new(|| Ok(Calculator::new()), Default::default(), config);
StreamableHttpService::new(|| Ok(Calculator::new()), session_manager, config);

@@ -75,2 +84,13 @@ let router = axum::Router::new().nest_service("/mcp", service);

async fn post_non_initialize(client: &reqwest::Client, url: &str) -> reqwest::Response {
client
.post(url)
.header("Content-Type", "application/json")
.header("Accept", "application/json, text/event-stream")
.body(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#)
.send()
.await
.expect("send non-initialize request")
}
#[tokio::test]

@@ -152,1 +172,19 @@ async fn stateless_init_rejects_when_header_older_than_body() -> anyhow::Result<()> {

}
#[tokio::test]
async fn stateful_rejected_initial_posts_do_not_create_sessions() -> anyhow::Result<()> {
let session_manager = Arc::new(LocalSessionManager::default());
let (client, url, ct) =
spawn_server_with_manager(stateful_config(), session_manager.clone()).await;
let response = post_non_initialize(&client, &url).await;
assert_eq!(response.status(), 422);
assert_eq!(session_manager.sessions.read().await.len(), 0);
let response = post_init(&client, &url, Some("2024-11-05"), "2025-11-25").await;
assert_eq!(response.status(), 400);
assert_eq!(session_manager.sessions.read().await.len(), 0);
ct.cancel();
Ok(())
}

@@ -6,3 +6,3 @@ #![allow(clippy::exhaustive_structs)]

handler::server::{router::tool::ToolRouter, tool::IntoCallToolResult, wrapper::Parameters},
model::{CallToolResult, Content, ServerResult, Tool},
model::{CallToolResult, ContentBlock, ServerResult, Tool},
tool, tool_handler, tool_router,

@@ -196,3 +196,4 @@ };

// Test that content and structured_content can both be passed separately
let content_result = CallToolResult::success(vec![Content::json(response.clone()).unwrap()]);
let content_result =
CallToolResult::success(vec![ContentBlock::json(response.clone()).unwrap()]);
let structured_result = CallToolResult::structured(json!({"message": "Hello"}));

@@ -199,0 +200,0 @@

@@ -14,3 +14,3 @@ #![cfg(not(feature = "local"))]

handler::server::router::tool::ToolRouter,
model::{CallToolRequestParams, ClientInfo, ErrorCode, JsonObject},
model::{CallToolRequestParams, ClientInfo, ErrorCode, TaskMetadata},
tool, tool_handler, tool_router,

@@ -22,2 +22,3 @@ };

pub struct TaskSupportTestServer {
#[expect(dead_code, reason = "tool_handler macro accesses this router field")]
tool_router: ToolRouter<Self>,

@@ -80,4 +81,4 @@ }

/// Helper to create a task object for tool calls
fn make_task() -> JsonObject {
serde_json::Map::new()
fn make_task() -> TaskMetadata {
TaskMetadata::new()
}

@@ -209,3 +210,3 @@

.first()
.and_then(|c| c.raw.as_text())
.and_then(|c| c.as_text())
.map(|t| t.text.as_str())

@@ -246,3 +247,3 @@ .unwrap_or("");

.first()
.and_then(|c| c.raw.as_text())
.and_then(|c| c.as_text())
.map(|t| t.text.as_str())

@@ -249,0 +250,0 @@ .unwrap_or("");

use std::{any::Any, time::Duration};
use rmcp::task_manager::{
OperationDescriptor, OperationMessage, OperationProcessor, OperationResultTransport,
use rmcp::{
model::TaskStatusNotificationParam,
task_manager::{
OperationDescriptor, OperationMessage, OperationProcessor, OperationResultTransport,
},
};
use serde_json::json;

@@ -78,1 +82,26 @@ struct DummyTransport {

}
#[test]
fn task_status_notification_param_preserves_meta() {
let raw = json!({
"_meta": {
"traceId": "trace-1"
},
"taskId": "task-1",
"status": "working",
"createdAt": "2026-06-24T00:00:00Z",
"lastUpdatedAt": "2026-06-24T00:00:01Z",
"ttl": null
});
let params: TaskStatusNotificationParam = serde_json::from_value(raw).unwrap();
assert_eq!(params.task.task_id, "task-1");
assert_eq!(params.task_id, "task-1");
assert_eq!(params.meta.as_ref().unwrap().0["traceId"], json!("trace-1"));
let serialized = serde_json::to_value(&params).unwrap();
assert_eq!(serialized["_meta"]["traceId"], json!("trace-1"));
assert_eq!(serialized["taskId"], json!("task-1"));
}

@@ -48,29 +48,22 @@ //! Integration tests for tool list change notifications.

fn call_tool(
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParams,
context: rmcp::service::RequestContext<RoleServer>,
) -> impl std::future::Future<Output = Result<CallToolResult, rmcp::ErrorData>> + MaybeSendFuture + '_
{
async move {
let router = self.router.read().await;
let tcc = ToolCallContext::new(self, request, context);
router.call(tcc).await
}
) -> Result<CallToolResult, rmcp::ErrorData> {
let router = self.router.read().await;
let tcc = ToolCallContext::new(self, request, context);
router.call(tcc).await
}
fn list_tools(
async fn list_tools(
&self,
_request: Option<rmcp::model::PaginatedRequestParams>,
_context: rmcp::service::RequestContext<RoleServer>,
) -> impl std::future::Future<Output = Result<rmcp::model::ListToolsResult, rmcp::ErrorData>>
+ MaybeSendFuture
+ '_ {
async move {
let router = self.router.read().await;
Ok(rmcp::model::ListToolsResult {
tools: router.list_all(),
..Default::default()
})
}
) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
let router = self.router.read().await;
Ok(rmcp::model::ListToolsResult {
tools: router.list_all(),
..Default::default()
})
}

@@ -77,0 +70,0 @@

@@ -21,7 +21,7 @@ #![cfg(not(feature = "local"))]

#[derive(Serialize, Deserialize, JsonSchema)]
pub struct GetWeatherRequest {
struct GetWeatherRequest {
/// City of interest.
pub city: String,
city: String,
/// Date of interest.
pub date: String,
date: String,
}

@@ -166,6 +166,6 @@

#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct OptionalFieldTestSchema {
struct OptionalFieldTestSchema {
/// Field description.
#[schemars(description = "An optional description field")]
pub description: Option<String>,
description: Option<String>,
}

@@ -175,9 +175,9 @@

#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct OptionalI64TestSchema {
struct OptionalI64TestSchema {
/// Optional count field.
#[schemars(description = "An optional i64 field")]
pub count: Option<i64>,
count: Option<i64>,
/// Added to ensure non-empty object schema.
pub mandatory_field: String,
mandatory_field: String,
}

@@ -331,3 +331,3 @@

.first()
.and_then(|content| content.raw.as_text())
.and_then(|content| content.as_text())
.map(|text| text.text.as_str())

@@ -359,3 +359,3 @@ .expect("Expected text content");

.first()
.and_then(|content| content.raw.as_text())
.and_then(|content| content.as_text())
.map(|text| text.text.as_str())

@@ -378,3 +378,3 @@ .expect("Expected text content");

#[derive(Debug, Clone)]
pub struct MinimalServer;
struct MinimalServer;

@@ -447,3 +447,3 @@ #[tool_router]

.first()
.and_then(|c| c.raw.as_text())
.and_then(|c| c.as_text())
.map(|t| t.text.as_str())

@@ -462,3 +462,3 @@ .expect("Expected text content");

#[derive(Debug, Clone)]
pub struct ElidedToolHandlerServer;
struct ElidedToolHandlerServer;

@@ -507,3 +507,3 @@ #[tool_router(server_handler)]

.first()
.and_then(|c| c.raw.as_text())
.and_then(|c| c.as_text())
.map(|t| t.text.as_str())

@@ -521,3 +521,3 @@ .expect("Expected text content");

#[derive(Debug, Clone)]
pub struct CustomInfoServer;
struct CustomInfoServer;

@@ -552,3 +552,3 @@ #[tool_router]

#[derive(Debug, Clone)]
pub struct ManualInfoServer;
struct ManualInfoServer;

@@ -555,0 +555,0 @@ #[tool_router]

@@ -1,2 +0,2 @@

use rmcp::model::{CallToolResult, Content, Meta};
use rmcp::model::{CallToolResult, ContentBlock, Meta};
use serde_json::{Value, json};

@@ -6,3 +6,3 @@

fn serialize_tool_result_with_meta() {
let content = vec![Content::text("ok")];
let content = vec![ContentBlock::text("ok")];
let mut meta = Meta::new();

@@ -37,3 +37,3 @@ meta.insert("foo".to_string(), json!("bar"));

fn serialize_tool_result_without_meta_omits_field() {
let result = CallToolResult::success(vec![Content::text("no meta")]);
let result = CallToolResult::success(vec![ContentBlock::text("no meta")]);
let v = serde_json::to_value(&result).unwrap();

@@ -40,0 +40,0 @@ // Ensure _meta is omitted

@@ -14,4 +14,4 @@ #![cfg(not(feature = "local"))]

#[derive(Debug, Default)]
pub struct TestHandler<T: 'static = ()> {
pub _marker: std::marker::PhantomData<fn(*const T)>,
struct TestHandler<T: 'static = ()> {
_marker: std::marker::PhantomData<fn(*const T)>,
}

@@ -21,10 +21,10 @@

#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
pub struct Request {
pub fields: HashMap<String, String>,
struct Request {
fields: HashMap<String, String>,
}
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
pub struct Sum {
pub a: i32,
pub b: i32,
struct Sum {
a: i32,
b: i32,
}

@@ -31,0 +31,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display