Skip to content

Commit aa4fef7

Browse files
committed
feat(rmcp): add MCP resource_link (2025-06-18)
1 parent 7135f25 commit aa4fef7

File tree

9 files changed

+1923
-119
lines changed

9 files changed

+1923
-119
lines changed

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ members = ["crates/rmcp", "crates/rmcp-macros", "examples/*"]
55
resolver = "2"
66

77
[workspace.dependencies]
8-
rmcp = { version = "0.6.0", path = "./crates/rmcp" }
9-
rmcp-macros = { version = "0.6.0", path = "./crates/rmcp-macros" }
8+
rmcp = { version = "0.5.0", path = "./crates/rmcp" }
9+
rmcp-macros = { version = "0.5.0", path = "./crates/rmcp-macros" }
1010

1111
[workspace.package]
1212
edition = "2024"
13-
version = "0.6.0"
13+
version = "0.5.0"
1414
authors = ["4t145 <[email protected]>"]
1515
license = "MIT/Apache-2.0"
1616
repository = "https://github.com/modelcontextprotocol/rust-sdk/"

crates/rmcp-macros/CHANGELOG.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
## [0.6.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v0.5.0...rmcp-macros-v0.6.0) - 2025-08-19
11-
12-
### Other
13-
14-
- add related project rustfs-mcp ([#378](https://github.com/modelcontextprotocol/rust-sdk/pull/378))
15-
1610
## [0.4.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v0.3.2...rmcp-macros-v0.4.0) - 2025-08-05
1711

1812
### Added

crates/rmcp/CHANGELOG.md

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
## [0.6.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-v0.5.0...rmcp-v0.6.0) - 2025-08-19
11-
12-
### Added
13-
14-
- Add MCP Elicitation support ([#332](https://github.com/modelcontextprotocol/rust-sdk/pull/332))
15-
- keep internal error in worker's quit reason ([#372](https://github.com/modelcontextprotocol/rust-sdk/pull/372))
16-
17-
### Fixed
18-
19-
- match shape of the calltoolresult schema ([#377](https://github.com/modelcontextprotocol/rust-sdk/pull/377))
20-
- make stdio shutdown more graceful ([#364](https://github.com/modelcontextprotocol/rust-sdk/pull/364))
21-
- *(tool)* remove unnecessary schema validation ([#375](https://github.com/modelcontextprotocol/rust-sdk/pull/375))
22-
- *(rmcp)* return serialized json with structured content ([#368](https://github.com/modelcontextprotocol/rust-sdk/pull/368))
23-
24-
### Other
25-
26-
- add related project rustfs-mcp ([#378](https://github.com/modelcontextprotocol/rust-sdk/pull/378))
27-
- *(streamable)* add document for extracting http info ([#373](https://github.com/modelcontextprotocol/rust-sdk/pull/373))
28-
2910
## [0.5.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-v0.4.1...rmcp-v0.5.0) - 2025-08-07
3011

3112
### Fixed

crates/rmcp/src/model/content.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
//! Content sent around agents, extensions, and LLMs
2+
//! NOTE: This file models MCP ContentBlock union types. Keep in sync with MCP draft schema.
3+
24
//! The various content types can be display to humans but also understood by models
35
//! They include optional annotations used to help inform agent usage
46
use serde::{Deserialize, Serialize};
@@ -13,6 +15,7 @@ pub struct RawTextContent {
1315
pub text: String,
1416
}
1517
pub type TextContent = Annotated<RawTextContent>;
18+
1619
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1720
#[serde(rename_all = "camelCase")]
1821
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@@ -56,8 +59,12 @@ pub type AudioContent = Annotated<RawAudioContent>;
5659
pub enum RawContent {
5760
Text(RawTextContent),
5861
Image(RawImageContent),
62+
/// Embedded resource payload
5963
Resource(RawEmbeddedResource),
60-
Audio(AudioContent),
64+
/// A link to a server resource that can be fetched on-demand
65+
#[serde(rename = "resource_link")]
66+
ResourceLink(super::resource::RawResource),
67+
Audio(RawAudioContent),
6168
}
6269

6370
pub type Content = Annotated<RawContent>;
@@ -75,6 +82,11 @@ impl RawContent {
7582
Ok(RawContent::text(json))
7683
}
7784

85+
/// Create a resource link content block
86+
pub fn resource_link(link: super::resource::RawResource) -> Self {
87+
RawContent::ResourceLink(link)
88+
}
89+
7890
pub fn text<S: Into<String>>(text: S) -> Self {
7991
RawContent::Text(RawTextContent { text: text.into() })
8092
}
@@ -94,7 +106,7 @@ impl RawContent {
94106
RawContent::Resource(RawEmbeddedResource {
95107
resource: ResourceContents::TextResourceContents {
96108
uri: uri.into(),
97-
mime_type: Some("text".to_string()),
109+
mime_type: Some("text/plain".to_string()),
98110
text: content.into(),
99111
},
100112
})
@@ -138,6 +150,10 @@ impl Content {
138150
RawContent::resource(resource).no_annotation()
139151
}
140152

153+
pub fn resource_link(link: super::resource::RawResource) -> Self {
154+
RawContent::resource_link(link).no_annotation()
155+
}
156+
141157
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
142158
RawContent::embedded_text(uri, content).no_annotation()
143159
}
@@ -207,4 +223,33 @@ mod tests {
207223
assert!(json.contains("mimeType"));
208224
assert!(!json.contains("mime_type"));
209225
}
226+
227+
#[test]
228+
fn test_resource_link_serialization() {
229+
use crate::model::resource::RawResource;
230+
231+
let link = RawResource {
232+
uri: "file:///example.pdf".to_string(),
233+
name: "Example PDF".to_string(),
234+
description: Some("A test PDF".to_string()),
235+
mime_type: Some("application/pdf".to_string()),
236+
size: Some(1234),
237+
};
238+
239+
let content = RawContent::resource_link(link);
240+
let json = serde_json::to_string(&content).unwrap();
241+
242+
assert!(json.contains("\"type\":\"resource_link\""));
243+
assert!(json.contains("\"uri\":\"file:///example.pdf\""));
244+
assert!(json.contains("\"name\":\"Example PDF\""));
245+
assert!(json.contains("mimeType"));
246+
assert!(!json.contains("mime_type"));
247+
}
248+
249+
#[test]
250+
fn test_embedded_text_uses_text_plain() {
251+
let content = RawContent::embedded_text("file:///example.txt", "hello");
252+
let json = serde_json::to_string(&content).unwrap();
253+
assert!(json.contains("\"mimeType\":\"text/plain\""));
254+
}
210255
}

crates/rmcp/src/model/prompt.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ pub enum PromptMessageContent {
7878
},
7979
/// Embedded server-side resource
8080
Resource { resource: EmbeddedResource },
81+
/// A link to a resource
82+
#[serde(rename = "resource_link")]
83+
ResourceLink {
84+
#[serde(flatten)]
85+
link: super::resource::Resource,
86+
},
8187
}
8288

8389
impl PromptMessageContent {
@@ -127,6 +133,20 @@ impl PromptMessage {
127133
}
128134
}
129135

136+
/// Create a new resource link message
137+
pub fn new_resource_link(
138+
role: PromptMessageRole,
139+
link: super::resource::RawResource,
140+
annotations: Option<Annotations>,
141+
) -> Self {
142+
Self {
143+
role,
144+
content: PromptMessageContent::ResourceLink {
145+
link: link.optional_annotate(annotations),
146+
},
147+
}
148+
}
149+
130150
/// Create a new resource message
131151
pub fn new_resource(
132152
role: PromptMessageRole,
@@ -174,3 +194,13 @@ mod tests {
174194
assert!(!json.contains("mime_type"));
175195
}
176196
}
197+
198+
#[test]
199+
fn test_prompt_message_resource_link() {
200+
use super::resource::RawResource;
201+
let link = RawResource::new("file:///example.txt", "example");
202+
let msg = PromptMessage::new_resource_link(PromptMessageRole::Assistant, link, None);
203+
let json = serde_json::to_string(&msg).unwrap();
204+
assert!(json.contains("\"type\":\"resource_link\""));
205+
assert!(json.contains("\"uri\":\"file:///example.txt\""));
206+
}

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
]
102102
},
103103
{
104+
"description": "Embedded resource payload",
104105
"type": "object",
105106
"properties": {
106107
"type": {
@@ -118,47 +119,40 @@
118119
]
119120
},
120121
{
122+
"description": "A link to a server resource that can be fetched on-demand",
121123
"type": "object",
122124
"properties": {
123125
"type": {
124126
"type": "string",
125-
"const": "audio"
127+
"const": "resource_link"
126128
}
127129
},
128130
"allOf": [
129131
{
130-
"$ref": "#/definitions/Annotated2"
132+
"$ref": "#/definitions/RawResource"
131133
}
132134
],
133135
"required": [
134136
"type"
135137
]
136-
}
137-
]
138-
},
139-
"Annotated2": {
140-
"type": "object",
141-
"properties": {
142-
"annotations": {
143-
"anyOf": [
144-
{
145-
"$ref": "#/definitions/Annotations"
146-
},
138+
},
139+
{
140+
"type": "object",
141+
"properties": {
142+
"type": {
143+
"type": "string",
144+
"const": "audio"
145+
}
146+
},
147+
"allOf": [
147148
{
148-
"type": "null"
149+
"$ref": "#/definitions/RawAudioContent"
149150
}
151+
],
152+
"required": [
153+
"type"
150154
]
151-
},
152-
"data": {
153-
"type": "string"
154-
},
155-
"mimeType": {
156-
"type": "string"
157155
}
158-
},
159-
"required": [
160-
"data",
161-
"mimeType"
162156
]
163157
},
164158
"Annotations": {
@@ -872,6 +866,21 @@
872866
"description": "Represents the MCP protocol version used for communication.\n\nThis ensures compatibility between clients and servers by specifying\nwhich version of the Model Context Protocol is being used.",
873867
"type": "string"
874868
},
869+
"RawAudioContent": {
870+
"type": "object",
871+
"properties": {
872+
"data": {
873+
"type": "string"
874+
},
875+
"mimeType": {
876+
"type": "string"
877+
}
878+
},
879+
"required": [
880+
"data",
881+
"mimeType"
882+
]
883+
},
875884
"RawEmbeddedResource": {
876885
"type": "object",
877886
"properties": {
@@ -899,6 +908,47 @@
899908
"mimeType"
900909
]
901910
},
911+
"RawResource": {
912+
"description": "Represents a resource in the extension with metadata",
913+
"type": "object",
914+
"properties": {
915+
"description": {
916+
"description": "Optional description of the resource",
917+
"type": [
918+
"string",
919+
"null"
920+
]
921+
},
922+
"mimeType": {
923+
"description": "MIME type of the resource content (\"text\" or \"blob\")",
924+
"type": [
925+
"string",
926+
"null"
927+
]
928+
},
929+
"name": {
930+
"description": "Name of the resource",
931+
"type": "string"
932+
},
933+
"size": {
934+
"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",
935+
"type": [
936+
"integer",
937+
"null"
938+
],
939+
"format": "uint32",
940+
"minimum": 0
941+
},
942+
"uri": {
943+
"description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")",
944+
"type": "string"
945+
}
946+
},
947+
"required": [
948+
"uri",
949+
"name"
950+
]
951+
},
902952
"RawTextContent": {
903953
"type": "object",
904954
"properties": {

0 commit comments

Comments
 (0)