Skip to content

feat: add MCP resource_link (2025-06-18) #381

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions crates/rmcp/src/model/content.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Content sent around agents, extensions, and LLMs
//! The various content types can be display to humans but also understood by models
//! NOTE: This file models MCP ContentBlock union types. Keep in sync with MCP draft schema.
Copy link
Preview

Copilot AI Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment refers to 'MCP draft schema' but the PR description indicates this aligns with MCP 2025-06-18. Consider updating to specify the exact schema version for clarity.

Suggested change
//! NOTE: This file models MCP ContentBlock union types. Keep in sync with MCP draft schema.
//! NOTE: This file models MCP ContentBlock union types. Keep in sync with MCP 2025-06-18 schema.

Copilot uses AI. Check for mistakes.


//! The various content types can be displayed to humans but also understood by models
//! They include optional annotations used to help inform agent usage
use serde::{Deserialize, Serialize};
use serde_json::json;
Expand All @@ -13,6 +15,7 @@ pub struct RawTextContent {
pub text: String,
}
pub type TextContent = Annotated<RawTextContent>;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
Expand Down Expand Up @@ -56,8 +59,12 @@ pub type AudioContent = Annotated<RawAudioContent>;
pub enum RawContent {
Text(RawTextContent),
Image(RawImageContent),
/// Embedded resource payload
Resource(RawEmbeddedResource),
Audio(AudioContent),
/// A link to a server resource that can be fetched on-demand
#[serde(rename = "resource_link")]
ResourceLink(super::resource::RawResource),
Audio(RawAudioContent),
}

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

/// Create a resource link content block
pub fn resource_link(link: super::resource::RawResource) -> Self {
RawContent::ResourceLink(link)
}

pub fn text<S: Into<String>>(text: S) -> Self {
RawContent::Text(RawTextContent { text: text.into() })
}
Expand All @@ -94,7 +106,7 @@ impl RawContent {
RawContent::Resource(RawEmbeddedResource {
resource: ResourceContents::TextResourceContents {
uri: uri.into(),
mime_type: Some("text".to_string()),
mime_type: Some("text/plain".to_string()),
text: content.into(),
},
})
Expand Down Expand Up @@ -138,6 +150,10 @@ impl Content {
RawContent::resource(resource).no_annotation()
}

pub fn resource_link(link: super::resource::RawResource) -> Self {
RawContent::resource_link(link).no_annotation()
}

pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
RawContent::embedded_text(uri, content).no_annotation()
}
Expand Down Expand Up @@ -207,4 +223,33 @@ mod tests {
assert!(json.contains("mimeType"));
assert!(!json.contains("mime_type"));
}

#[test]
fn test_resource_link_serialization() {
use crate::model::resource::RawResource;

let link = RawResource {
uri: "file:///example.pdf".to_string(),
name: "Example PDF".to_string(),
description: Some("A test PDF".to_string()),
mime_type: Some("application/pdf".to_string()),
size: Some(1234),
};

let content = RawContent::resource_link(link);
let json = serde_json::to_string(&content).unwrap();

assert!(json.contains("\"type\":\"resource_link\""));
assert!(json.contains("\"uri\":\"file:///example.pdf\""));
assert!(json.contains("\"name\":\"Example PDF\""));
assert!(json.contains("mimeType"));
assert!(!json.contains("mime_type"));
}

#[test]
fn test_embedded_text_uses_text_plain() {
let content = RawContent::embedded_text("file:///example.txt", "hello");
let json = serde_json::to_string(&content).unwrap();
assert!(json.contains("\"mimeType\":\"text/plain\""));
}
}
30 changes: 30 additions & 0 deletions crates/rmcp/src/model/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ pub enum PromptMessageContent {
},
/// Embedded server-side resource
Resource { resource: EmbeddedResource },
/// A link to a resource
#[serde(rename = "resource_link")]
ResourceLink {
#[serde(flatten)]
link: super::resource::Resource,
},
}

impl PromptMessageContent {
Expand Down Expand Up @@ -127,6 +133,20 @@ impl PromptMessage {
}
}

/// Create a new resource link message
pub fn new_resource_link(
role: PromptMessageRole,
link: super::resource::RawResource,
annotations: Option<Annotations>,
) -> Self {
Self {
role,
content: PromptMessageContent::ResourceLink {
link: link.optional_annotate(annotations),
},
}
}

/// Create a new resource message
pub fn new_resource(
role: PromptMessageRole,
Expand Down Expand Up @@ -174,3 +194,13 @@ mod tests {
assert!(!json.contains("mime_type"));
}
}

#[test]
fn test_prompt_message_resource_link() {
use super::resource::RawResource;
let link = RawResource::new("file:///example.txt", "example");
let msg = PromptMessage::new_resource_link(PromptMessageRole::Assistant, link, None);
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"resource_link\""));
assert!(json.contains("\"uri\":\"file:///example.txt\""));
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
]
},
{
"description": "Embedded resource payload",
"type": "object",
"properties": {
"type": {
Expand All @@ -118,47 +119,40 @@
]
},
{
"description": "A link to a server resource that can be fetched on-demand",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "audio"
"const": "resource_link"
}
},
"allOf": [
{
"$ref": "#/definitions/Annotated2"
"$ref": "#/definitions/RawResource"
}
],
"required": [
"type"
]
}
]
},
"Annotated2": {
"type": "object",
"properties": {
"annotations": {
"anyOf": [
{
"$ref": "#/definitions/Annotations"
},
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "audio"
}
},
"allOf": [
{
"type": "null"
"$ref": "#/definitions/RawAudioContent"
}
],
"required": [
"type"
]
},
"data": {
"type": "string"
},
"mimeType": {
"type": "string"
}
},
"required": [
"data",
"mimeType"
]
},
"Annotations": {
Expand Down Expand Up @@ -872,6 +866,21 @@
"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.",
"type": "string"
},
"RawAudioContent": {
"type": "object",
"properties": {
"data": {
"type": "string"
},
"mimeType": {
"type": "string"
}
},
"required": [
"data",
"mimeType"
]
},
"RawEmbeddedResource": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -899,6 +908,47 @@
"mimeType"
]
},
"RawResource": {
"description": "Represents a resource in the extension with metadata",
"type": "object",
"properties": {
"description": {
"description": "Optional description of the resource",
"type": [
"string",
"null"
]
},
"mimeType": {
"description": "MIME type of the resource content (\"text\" or \"blob\")",
"type": [
"string",
"null"
]
},
"name": {
"description": "Name of the resource",
"type": "string"
},
"size": {
"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",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"uri": {
"description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")",
"type": "string"
}
},
"required": [
"uri",
"name"
]
},
"RawTextContent": {
"type": "object",
"properties": {
Expand Down
Loading