Skip to content

Commit 1b70f5b

Browse files
authored
feat: add title field for data types (#410)
* feat(model): add title field to various structs * fix(test): fix test json schema * fix(model): allow boxed type in `ts_union` macro
1 parent fdd6bc0 commit 1b70f5b

File tree

16 files changed

+219
-73
lines changed

16 files changed

+219
-73
lines changed

crates/rmcp-macros/src/prompt.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use crate::common::{extract_doc_line, none_expr};
1010
pub struct PromptAttribute {
1111
/// The name of the prompt
1212
pub name: Option<String>,
13+
/// Human readable title of prompt
14+
pub title: Option<String>,
1315
/// Optional description of what the prompt does
1416
pub description: Option<String>,
1517
/// Arguments that can be passed to the prompt
@@ -18,6 +20,7 @@ pub struct PromptAttribute {
1820

1921
pub struct ResolvedPromptAttribute {
2022
pub name: String,
23+
pub title: Option<String>,
2124
pub description: Option<String>,
2225
pub arguments: Expr,
2326
}
@@ -28,18 +31,25 @@ impl ResolvedPromptAttribute {
2831
name,
2932
description,
3033
arguments,
34+
title,
3135
} = self;
3236
let description = if let Some(description) = description {
3337
quote! { Some(#description.into()) }
3438
} else {
3539
quote! { None }
3640
};
41+
let title = if let Some(title) = title {
42+
quote! { Some(#title.into()) }
43+
} else {
44+
quote! { None }
45+
};
3746
let tokens = quote! {
3847
pub fn #fn_ident() -> rmcp::model::Prompt {
3948
rmcp::model::Prompt {
4049
name: #name.into(),
4150
description: #description,
4251
arguments: #arguments,
52+
title: #title,
4353
}
4454
}
4555
};
@@ -87,6 +97,7 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream>
8797
name: name.clone(),
8898
description: description.clone(),
8999
arguments: arguments.clone(),
100+
title: attribute.title,
90101
};
91102
let prompt_attr_fn = resolved_prompt_attr.into_fn(prompt_attr_fn_ident.clone())?;
92103

crates/rmcp-macros/src/tool.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ fn extract_schema_from_return_type(ret_type: &syn::Type) -> Option<Expr> {
6666
pub struct ToolAttribute {
6767
/// The name of the tool
6868
pub name: Option<String>,
69+
/// Human readable title of tool
70+
pub title: Option<String>,
6971
pub description: Option<String>,
7072
/// A JSON Schema object defining the expected parameters for the tool
7173
pub input_schema: Option<Expr>,
@@ -77,6 +79,7 @@ pub struct ToolAttribute {
7779

7880
pub struct ResolvedToolAttribute {
7981
pub name: String,
82+
pub title: Option<String>,
8083
pub description: Option<String>,
8184
pub input_schema: Expr,
8285
pub output_schema: Option<Expr>,
@@ -88,6 +91,7 @@ impl ResolvedToolAttribute {
8891
let Self {
8992
name,
9093
description,
94+
title,
9195
input_schema,
9296
output_schema,
9397
annotations,
@@ -102,10 +106,16 @@ impl ResolvedToolAttribute {
102106
} else {
103107
quote! { None }
104108
};
109+
let title = if let Some(title) = title {
110+
quote! { Some(#title.into()) }
111+
} else {
112+
quote! { None }
113+
};
105114
let tokens = quote! {
106115
pub fn #fn_ident() -> rmcp::model::Tool {
107116
rmcp::model::Tool {
108117
name: #name.into(),
118+
title: #title,
109119
description: #description,
110120
input_schema: #input_schema,
111121
output_schema: #output_schema,
@@ -229,6 +239,7 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
229239
input_schema: input_schema_expr,
230240
output_schema: output_schema_expr,
231241
annotations: annotations_expr,
242+
title: attribute.title,
232243
};
233244
let tool_attr_fn = resolved_tool_attr.into_fn(tool_attr_fn_ident)?;
234245
// modify the the input function

crates/rmcp/src/handler/client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ impl<H: ClientHandler> Service<RoleClient> for H {
1616
ServerRequest::CreateMessageRequest(request) => self
1717
.create_message(request.params, context)
1818
.await
19+
.map(Box::new)
1920
.map(ClientResult::CreateMessageResult),
2021
ServerRequest::ListRootsRequest(_) => self
2122
.list_roots(context)

crates/rmcp/src/handler/server/prompt.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ pub fn cached_arguments_from_schema<T: schemars::JsonSchema + std::any::Any>()
350350

351351
arguments.push(crate::model::PromptArgument {
352352
name: name.clone(),
353+
title: None,
353354
description,
354355
required: Some(required.contains(name.as_str())),
355356
});

crates/rmcp/src/model.rs

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ impl ErrorData {
504504
/// Represents any JSON-RPC message that can be sent or received.
505505
///
506506
/// This enum covers all possible message types in the JSON-RPC protocol:
507-
/// individual requests/responses, notifications, batch operations, and errors.
507+
/// individual requests/responses, notifications, and errors.
508508
/// It serves as the top-level message container for MCP communication.
509509
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
510510
#[serde(untagged)]
@@ -697,6 +697,8 @@ impl Default for ClientInfo {
697697
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
698698
pub struct Implementation {
699699
pub name: String,
700+
#[serde(skip_serializing_if = "Option::is_none")]
701+
pub title: Option<String>,
700702
pub version: String,
701703
}
702704

@@ -710,6 +712,7 @@ impl Implementation {
710712
pub fn from_build_env() -> Self {
711713
Implementation {
712714
name: env!("CARGO_CRATE_NAME").to_owned(),
715+
title: None,
713716
version: env!("CARGO_PKG_VERSION").to_owned(),
714717
}
715718
}
@@ -1115,6 +1118,8 @@ pub struct ResourceReference {
11151118
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
11161119
pub struct PromptReference {
11171120
pub name: String,
1121+
#[serde(skip_serializing_if = "Option::is_none")]
1122+
pub title: Option<String>,
11181123
}
11191124

11201125
const_string!(CompleteRequestMethod = "completion/complete");
@@ -1449,24 +1454,50 @@ pub struct GetPromptResult {
14491454

14501455
macro_rules! ts_union {
14511456
(
1452-
export type $U: ident =
1453-
$(|)?$($V: ident)|*;
1457+
export type $U:ident =
1458+
$($rest:tt)*
14541459
) => {
1460+
ts_union!(@declare $U { $($rest)* });
1461+
ts_union!(@impl_from $U { $($rest)* });
1462+
};
1463+
(@declare $U:ident { $($variant:tt)* }) => {
1464+
ts_union!(@declare_variant $U { } {$($variant)*} );
1465+
};
1466+
(@declare_variant $U:ident { $($declared:tt)* } {$(|)? box $V:ident $($rest:tt)*}) => {
1467+
ts_union!(@declare_variant $U { $($declared)* $V(Box<$V>), } {$($rest)*});
1468+
};
1469+
(@declare_variant $U:ident { $($declared:tt)* } {$(|)? $V:ident $($rest:tt)*}) => {
1470+
ts_union!(@declare_variant $U { $($declared)* $V($V), } {$($rest)*});
1471+
};
1472+
(@declare_variant $U:ident { $($declared:tt)* } { ; }) => {
1473+
ts_union!(@declare_end $U { $($declared)* } );
1474+
};
1475+
(@declare_end $U:ident { $($declared:tt)* }) => {
14551476
#[derive(Debug, Serialize, Deserialize, Clone)]
14561477
#[serde(untagged)]
14571478
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
14581479
pub enum $U {
1459-
$($V($V),)*
1480+
$($declared)*
14601481
}
1461-
1462-
$(
1463-
impl From<$V> for $U {
1464-
fn from(value: $V) -> Self {
1465-
$U::$V(value)
1466-
}
1482+
};
1483+
(@impl_from $U: ident {$(|)? box $V:ident $($rest:tt)*}) => {
1484+
impl From<$V> for $U {
1485+
fn from(value: $V) -> Self {
1486+
$U::$V(Box::new(value))
14671487
}
1468-
)*
1488+
}
1489+
ts_union!(@impl_from $U {$($rest)*});
1490+
};
1491+
(@impl_from $U: ident {$(|)? $V:ident $($rest:tt)*}) => {
1492+
impl From<$V> for $U {
1493+
fn from(value: $V) -> Self {
1494+
$U::$V(value)
1495+
}
1496+
}
1497+
ts_union!(@impl_from $U {$($rest)*});
14691498
};
1499+
(@impl_from $U: ident { ; }) => {};
1500+
(@impl_from $U: ident { }) => {};
14701501
}
14711502

14721503
ts_union!(
@@ -1515,7 +1546,7 @@ ts_union!(
15151546
);
15161547

15171548
ts_union!(
1518-
export type ClientResult = CreateMessageResult | ListRootsResult | CreateElicitationResult | EmptyResult;
1549+
export type ClientResult = box CreateMessageResult | ListRootsResult | CreateElicitationResult | EmptyResult;
15191550
);
15201551

15211552
impl ClientResult {

crates/rmcp/src/model/content.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pub enum RawContent {
6666
Text(RawTextContent),
6767
Image(RawImageContent),
6868
Resource(RawEmbeddedResource),
69-
Audio(AudioContent),
69+
Audio(RawAudioContent),
7070
ResourceLink(super::resource::RawResource),
7171
}
7272

@@ -253,6 +253,7 @@ mod tests {
253253
let resource_link = RawContent::ResourceLink(RawResource {
254254
uri: "file:///test.txt".to_string(),
255255
name: "test.txt".to_string(),
256+
title: None,
256257
description: Some("A test file".to_string()),
257258
mime_type: Some("text/plain".to_string()),
258259
size: Some(100),

crates/rmcp/src/model/prompt.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use super::{
1414
pub struct Prompt {
1515
/// The name of the prompt
1616
pub name: String,
17+
#[serde(skip_serializing_if = "Option::is_none")]
18+
pub title: Option<String>,
1719
/// Optional description of what the prompt does
1820
#[serde(skip_serializing_if = "Option::is_none")]
1921
pub description: Option<String>,
@@ -35,6 +37,7 @@ impl Prompt {
3537
{
3638
Prompt {
3739
name: name.into(),
40+
title: None,
3841
description: description.map(Into::into),
3942
arguments,
4043
}
@@ -47,6 +50,9 @@ impl Prompt {
4750
pub struct PromptArgument {
4851
/// The name of the argument
4952
pub name: String,
53+
/// A human-readable title for the argument
54+
#[serde(skip_serializing_if = "Option::is_none")]
55+
pub title: Option<String>,
5056
/// A description of what the argument is used for
5157
#[serde(skip_serializing_if = "Option::is_none")]
5258
pub description: Option<String>,

crates/rmcp/src/model/resource.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ pub struct RawResource {
1111
pub uri: String,
1212
/// Name of the resource
1313
pub name: String,
14+
/// Human-readable title of the resource
15+
#[serde(skip_serializing_if = "Option::is_none")]
16+
pub title: Option<String>,
1417
/// Optional description of the resource
1518
#[serde(skip_serializing_if = "Option::is_none")]
1619
pub description: Option<String>,
@@ -34,6 +37,8 @@ pub struct RawResourceTemplate {
3437
pub uri_template: String,
3538
pub name: String,
3639
#[serde(skip_serializing_if = "Option::is_none")]
40+
pub title: Option<String>,
41+
#[serde(skip_serializing_if = "Option::is_none")]
3742
pub description: Option<String>,
3843
#[serde(skip_serializing_if = "Option::is_none")]
3944
pub mime_type: Option<String>,
@@ -82,6 +87,7 @@ impl RawResource {
8287
Self {
8388
uri: uri.into(),
8489
name: name.into(),
90+
title: None,
8591
description: None,
8692
mime_type: None,
8793
size: None,
@@ -99,6 +105,7 @@ mod tests {
99105
fn test_resource_serialization() {
100106
let resource = RawResource {
101107
uri: "file:///test.txt".to_string(),
108+
title: None,
102109
name: "test".to_string(),
103110
description: Some("Test resource".to_string()),
104111
mime_type: Some("text/plain".to_string()),

crates/rmcp/src/model/tool.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ use super::JsonObject;
1515
pub struct Tool {
1616
/// The name of the tool
1717
pub name: Cow<'static, str>,
18+
/// A human-readable title for the tool
19+
#[serde(skip_serializing_if = "Option::is_none")]
20+
pub title: Option<String>,
1821
/// A description of what the tool does
1922
#[serde(skip_serializing_if = "Option::is_none")]
2023
pub description: Option<Cow<'static, str>>,
@@ -138,6 +141,7 @@ impl Tool {
138141
{
139142
Tool {
140143
name: name.into(),
144+
title: None,
141145
description: Some(description.into()),
142146
input_schema: input_schema.into(),
143147
output_schema: None,

crates/rmcp/src/service/server.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,22 @@ macro_rules! method {
384384
}
385385

386386
impl Peer<RoleServer> {
387-
method!(peer_req create_message CreateMessageRequest(CreateMessageRequestParam) => CreateMessageResult);
387+
pub async fn create_message(
388+
&self,
389+
params: CreateMessageRequestParam,
390+
) -> Result<CreateMessageResult, ServiceError> {
391+
let result = self
392+
.send_request(ServerRequest::CreateMessageRequest(CreateMessageRequest {
393+
method: Default::default(),
394+
params,
395+
extensions: Default::default(),
396+
}))
397+
.await?;
398+
match result {
399+
ClientResult::CreateMessageResult(result) => Ok(*result),
400+
_ => Err(ServiceError::UnexpectedResponse),
401+
}
402+
}
388403
method!(peer_req list_roots ListRootsRequest() => ListRootsResult);
389404
#[cfg(feature = "elicitation")]
390405
method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParam) => CreateElicitationResult);

0 commit comments

Comments
 (0)