Skip to content

Commit 98b77fd

Browse files
authored
feat: add resource_link support to tools and prompts (#381)
* feat: add resource_link support to tools and prompts * chore: remove unused serde_json import from test_resource_link_integration.rs * chore: remove unused serde_json import from test_resource_link.rs
1 parent d328157 commit 98b77fd

File tree

8 files changed

+2129
-7
lines changed

8 files changed

+2129
-7
lines changed

crates/rmcp/src/model/content.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@ pub struct RawAudioContent {
5151
pub type AudioContent = Annotated<RawAudioContent>;
5252

5353
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54-
#[serde(tag = "type", rename_all = "camelCase")]
54+
#[serde(tag = "type", rename_all = "snake_case")]
5555
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
5656
pub enum RawContent {
5757
Text(RawTextContent),
5858
Image(RawImageContent),
5959
Resource(RawEmbeddedResource),
6060
Audio(AudioContent),
61+
ResourceLink(super::resource::RawResource),
6162
}
6263

6364
pub type Content = Annotated<RawContent>;
@@ -123,6 +124,19 @@ impl RawContent {
123124
_ => None,
124125
}
125126
}
127+
128+
/// Get the resource link if this is a ResourceLink variant
129+
pub fn as_resource_link(&self) -> Option<&super::resource::RawResource> {
130+
match self {
131+
RawContent::ResourceLink(link) => Some(link),
132+
_ => None,
133+
}
134+
}
135+
136+
/// Create a resource link content
137+
pub fn resource_link(resource: super::resource::RawResource) -> Self {
138+
RawContent::ResourceLink(resource)
139+
}
126140
}
127141

128142
impl Content {
@@ -145,6 +159,11 @@ impl Content {
145159
pub fn json<S: Serialize>(json: S) -> Result<Self, crate::ErrorData> {
146160
RawContent::json(json).map(|c| c.no_annotation())
147161
}
162+
163+
/// Create a resource link content
164+
pub fn resource_link(resource: super::resource::RawResource) -> Self {
165+
RawContent::resource_link(resource).no_annotation()
166+
}
148167
}
149168

150169
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -207,4 +226,47 @@ mod tests {
207226
assert!(json.contains("mimeType"));
208227
assert!(!json.contains("mime_type"));
209228
}
229+
230+
#[test]
231+
fn test_resource_link_serialization() {
232+
use super::super::resource::RawResource;
233+
234+
let resource_link = RawContent::ResourceLink(RawResource {
235+
uri: "file:///test.txt".to_string(),
236+
name: "test.txt".to_string(),
237+
description: Some("A test file".to_string()),
238+
mime_type: Some("text/plain".to_string()),
239+
size: Some(100),
240+
});
241+
242+
let json = serde_json::to_string(&resource_link).unwrap();
243+
println!("ResourceLink JSON: {}", json);
244+
245+
// Verify it contains the correct type tag
246+
assert!(json.contains("\"type\":\"resource_link\""));
247+
assert!(json.contains("\"uri\":\"file:///test.txt\""));
248+
assert!(json.contains("\"name\":\"test.txt\""));
249+
}
250+
251+
#[test]
252+
fn test_resource_link_deserialization() {
253+
let json = r#"{
254+
"type": "resource_link",
255+
"uri": "file:///example.txt",
256+
"name": "example.txt",
257+
"description": "Example file",
258+
"mimeType": "text/plain"
259+
}"#;
260+
261+
let content: RawContent = serde_json::from_str(json).unwrap();
262+
263+
if let RawContent::ResourceLink(resource) = content {
264+
assert_eq!(resource.uri, "file:///example.txt");
265+
assert_eq!(resource.name, "example.txt");
266+
assert_eq!(resource.description, Some("Example file".to_string()));
267+
assert_eq!(resource.mime_type, Some("text/plain".to_string()));
268+
} else {
269+
panic!("Expected ResourceLink variant");
270+
}
271+
}
210272
}

crates/rmcp/src/model/prompt.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pub enum PromptMessageRole {
6666

6767
/// Content types that can be included in prompt messages
6868
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69-
#[serde(tag = "type", rename_all = "camelCase")]
69+
#[serde(tag = "type", rename_all = "snake_case")]
7070
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
7171
pub enum PromptMessageContent {
7272
/// Plain text content
@@ -78,12 +78,22 @@ pub enum PromptMessageContent {
7878
},
7979
/// Embedded server-side resource
8080
Resource { resource: EmbeddedResource },
81+
/// A link to a resource that can be fetched separately
82+
ResourceLink {
83+
#[serde(flatten)]
84+
link: super::resource::Resource,
85+
},
8186
}
8287

8388
impl PromptMessageContent {
8489
pub fn text(text: impl Into<String>) -> Self {
8590
Self::Text { text: text.into() }
8691
}
92+
93+
/// Create a resource link content
94+
pub fn resource_link(resource: super::resource::Resource) -> Self {
95+
Self::ResourceLink { link: resource }
96+
}
8797
}
8898

8999
/// A message in a prompt conversation
@@ -151,6 +161,14 @@ impl PromptMessage {
151161
},
152162
}
153163
}
164+
165+
/// Create a new resource link message
166+
pub fn new_resource_link(role: PromptMessageRole, resource: super::resource::Resource) -> Self {
167+
Self {
168+
role,
169+
content: PromptMessageContent::ResourceLink { link: resource },
170+
}
171+
}
154172
}
155173

156174
#[cfg(test)]
@@ -173,4 +191,43 @@ mod tests {
173191
assert!(json.contains("mimeType"));
174192
assert!(!json.contains("mime_type"));
175193
}
194+
195+
#[test]
196+
fn test_prompt_message_resource_link_serialization() {
197+
use super::super::resource::RawResource;
198+
199+
let resource = RawResource::new("file:///test.txt", "test.txt");
200+
let message =
201+
PromptMessage::new_resource_link(PromptMessageRole::User, resource.no_annotation());
202+
203+
let json = serde_json::to_string(&message).unwrap();
204+
println!("PromptMessage with ResourceLink JSON: {}", json);
205+
206+
// Verify it contains the correct type tag
207+
assert!(json.contains("\"type\":\"resource_link\""));
208+
assert!(json.contains("\"uri\":\"file:///test.txt\""));
209+
assert!(json.contains("\"name\":\"test.txt\""));
210+
}
211+
212+
#[test]
213+
fn test_prompt_message_content_resource_link_deserialization() {
214+
let json = r#"{
215+
"type": "resource_link",
216+
"uri": "file:///example.txt",
217+
"name": "example.txt",
218+
"description": "Example file",
219+
"mimeType": "text/plain"
220+
}"#;
221+
222+
let content: PromptMessageContent = serde_json::from_str(json).unwrap();
223+
224+
if let PromptMessageContent::ResourceLink { link } = content {
225+
assert_eq!(link.uri, "file:///example.txt");
226+
assert_eq!(link.name, "example.txt");
227+
assert_eq!(link.description, Some("Example file".to_string()));
228+
assert_eq!(link.mime_type, Some("text/plain".to_string()));
229+
} else {
230+
panic!("Expected ResourceLink variant");
231+
}
232+
}
176233
}

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,23 @@
133133
"required": [
134134
"type"
135135
]
136+
},
137+
{
138+
"type": "object",
139+
"properties": {
140+
"type": {
141+
"type": "string",
142+
"const": "resource_link"
143+
}
144+
},
145+
"allOf": [
146+
{
147+
"$ref": "#/definitions/RawResource"
148+
}
149+
],
150+
"required": [
151+
"type"
152+
]
136153
}
137154
]
138155
},
@@ -899,6 +916,47 @@
899916
"mimeType"
900917
]
901918
},
919+
"RawResource": {
920+
"description": "Represents a resource in the extension with metadata",
921+
"type": "object",
922+
"properties": {
923+
"description": {
924+
"description": "Optional description of the resource",
925+
"type": [
926+
"string",
927+
"null"
928+
]
929+
},
930+
"mimeType": {
931+
"description": "MIME type of the resource content (\"text\" or \"blob\")",
932+
"type": [
933+
"string",
934+
"null"
935+
]
936+
},
937+
"name": {
938+
"description": "Name of the resource",
939+
"type": "string"
940+
},
941+
"size": {
942+
"description": "The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.\n\nThis can be used by Hosts to display file sizes and estimate context window us",
943+
"type": [
944+
"integer",
945+
"null"
946+
],
947+
"format": "uint32",
948+
"minimum": 0
949+
},
950+
"uri": {
951+
"description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")",
952+
"type": "string"
953+
}
954+
},
955+
"required": [
956+
"uri",
957+
"name"
958+
]
959+
},
902960
"RawTextContent": {
903961
"type": "object",
904962
"properties": {

0 commit comments

Comments
 (0)