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
This commit is contained in:
parent
d328157be3
commit
98b77fd94a
8 changed files with 2129 additions and 7 deletions
|
|
@ -51,13 +51,14 @@ pub struct RawAudioContent {
|
|||
pub type AudioContent = Annotated<RawAudioContent>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum RawContent {
|
||||
Text(RawTextContent),
|
||||
Image(RawImageContent),
|
||||
Resource(RawEmbeddedResource),
|
||||
Audio(AudioContent),
|
||||
ResourceLink(super::resource::RawResource),
|
||||
}
|
||||
|
||||
pub type Content = Annotated<RawContent>;
|
||||
|
|
@ -123,6 +124,19 @@ impl RawContent {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the resource link if this is a ResourceLink variant
|
||||
pub fn as_resource_link(&self) -> Option<&super::resource::RawResource> {
|
||||
match self {
|
||||
RawContent::ResourceLink(link) => Some(link),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a resource link content
|
||||
pub fn resource_link(resource: super::resource::RawResource) -> Self {
|
||||
RawContent::ResourceLink(resource)
|
||||
}
|
||||
}
|
||||
|
||||
impl Content {
|
||||
|
|
@ -145,6 +159,11 @@ impl Content {
|
|||
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)]
|
||||
|
|
@ -207,4 +226,47 @@ mod tests {
|
|||
assert!(json.contains("mimeType"));
|
||||
assert!(!json.contains("mime_type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_link_serialization() {
|
||||
use super::super::resource::RawResource;
|
||||
|
||||
let resource_link = RawContent::ResourceLink(RawResource {
|
||||
uri: "file:///test.txt".to_string(),
|
||||
name: "test.txt".to_string(),
|
||||
description: Some("A test file".to_string()),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
size: Some(100),
|
||||
});
|
||||
|
||||
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\""));
|
||||
assert!(json.contains("\"uri\":\"file:///test.txt\""));
|
||||
assert!(json.contains("\"name\":\"test.txt\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_link_deserialization() {
|
||||
let json = r#"{
|
||||
"type": "resource_link",
|
||||
"uri": "file:///example.txt",
|
||||
"name": "example.txt",
|
||||
"description": "Example file",
|
||||
"mimeType": "text/plain"
|
||||
}"#;
|
||||
|
||||
let content: RawContent = serde_json::from_str(json).unwrap();
|
||||
|
||||
if let RawContent::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 {
|
||||
panic!("Expected ResourceLink variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ pub enum PromptMessageRole {
|
|||
|
||||
/// Content types that can be included in prompt messages
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum PromptMessageContent {
|
||||
/// Plain text content
|
||||
|
|
@ -78,12 +78,22 @@ pub enum PromptMessageContent {
|
|||
},
|
||||
/// Embedded server-side resource
|
||||
Resource { 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
|
||||
|
|
@ -151,6 +161,14 @@ impl PromptMessage {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new resource link message
|
||||
pub fn new_resource_link(role: PromptMessageRole, resource: super::resource::Resource) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content: PromptMessageContent::ResourceLink { link: resource },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -173,4 +191,43 @@ mod tests {
|
|||
assert!(json.contains("mimeType"));
|
||||
assert!(!json.contains("mime_type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prompt_message_resource_link_serialization() {
|
||||
use super::super::resource::RawResource;
|
||||
|
||||
let resource = RawResource::new("file:///test.txt", "test.txt");
|
||||
let message =
|
||||
PromptMessage::new_resource_link(PromptMessageRole::User, resource.no_annotation());
|
||||
|
||||
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\""));
|
||||
assert!(json.contains("\"uri\":\"file:///test.txt\""));
|
||||
assert!(json.contains("\"name\":\"test.txt\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prompt_message_content_resource_link_deserialization() {
|
||||
let json = r#"{
|
||||
"type": "resource_link",
|
||||
"uri": "file:///example.txt",
|
||||
"name": "example.txt",
|
||||
"description": "Example file",
|
||||
"mimeType": "text/plain"
|
||||
}"#;
|
||||
|
||||
let content: PromptMessageContent = 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()));
|
||||
} else {
|
||||
panic!("Expected ResourceLink variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,23 @@
|
|||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "resource_link"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawResource"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -899,6 +916,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": {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -133,6 +133,23 @@
|
|||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "resource_link"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawResource"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1278,6 +1295,62 @@
|
|||
"type",
|
||||
"resource"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A link to a resource that can be fetched separately",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Annotations"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "resource_link"
|
||||
},
|
||||
"uri": {
|
||||
"description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"uri",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1331,6 +1404,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": {
|
||||
|
|
|
|||
|
|
@ -133,6 +133,23 @@
|
|||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "resource_link"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RawResource"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -304,10 +321,7 @@
|
|||
"properties": {
|
||||
"content": {
|
||||
"description": "The content returned by the tool (text, images, etc.)",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Annotated"
|
||||
}
|
||||
|
|
@ -322,7 +336,10 @@
|
|||
"structuredContent": {
|
||||
"description": "An optional JSON object that represents the structured result of the tool call"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content"
|
||||
]
|
||||
},
|
||||
"CancelledNotificationMethod": {
|
||||
"type": "string",
|
||||
|
|
@ -405,6 +422,45 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CreateElicitationRequestParam": {
|
||||
"description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A JSON schema defining the expected structure of the response",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"description": "Human-readable message explaining what input is needed from the user.\nThis should be clear and provide sufficient context for the user to understand\nwhat information they need to provide.",
|
||||
"type": "string"
|
||||
},
|
||||
"requestedSchema": {
|
||||
"description": "JSON Schema defining the expected structure and validation rules for the user's response.\nThis allows clients to validate input and provide appropriate UI controls.\nMust be a valid JSON Schema Draft 2020-12 object.",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"requestedSchema"
|
||||
]
|
||||
},
|
||||
"CreateElicitationResult": {
|
||||
"description": "The result returned by a client in response to an elicitation request.\n\nContains the user's decision (accept/decline/cancel) and optionally their input data\nif they chose to accept the request.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"description": "The user's decision on how to handle the elicitation request",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ElicitationAction"
|
||||
}
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
"description": "The actual data provided by the user, if they accepted the request.\nMust conform to the JSON schema specified in the original request.\nOnly present when action is Accept."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
]
|
||||
},
|
||||
"CreateMessageRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -483,6 +539,31 @@
|
|||
"maxTokens"
|
||||
]
|
||||
},
|
||||
"ElicitationAction": {
|
||||
"description": "Represents the possible actions a user can take in response to an elicitation request.\n\nWhen a server requests user input through elicitation, the user can:\n- Accept: Provide the requested information and continue\n- Decline: Refuse to provide the information but continue the operation\n- Cancel: Stop the entire operation",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "User accepts the request and provides the requested information",
|
||||
"type": "string",
|
||||
"const": "accept"
|
||||
},
|
||||
{
|
||||
"description": "User declines to provide the information but allows the operation to continue",
|
||||
"type": "string",
|
||||
"const": "decline"
|
||||
},
|
||||
{
|
||||
"description": "User cancels the entire operation",
|
||||
"type": "string",
|
||||
"const": "cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ElicitationCreateRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "elicitation/create"
|
||||
},
|
||||
"EmptyObject": {
|
||||
"description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.",
|
||||
"type": "object"
|
||||
|
|
@ -686,6 +767,9 @@
|
|||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestNoParam2"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request2"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
|
|
@ -1211,6 +1295,62 @@
|
|||
"type",
|
||||
"resource"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A link to a resource that can be fetched separately",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Annotations"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "resource_link"
|
||||
},
|
||||
"uri": {
|
||||
"description": "URI representing the resource location (e.g., \"file:///path/to/file\" or \"str:///content\")",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"uri",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1264,6 +1404,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": {
|
||||
|
|
@ -1307,6 +1488,22 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Request2": {
|
||||
"description": "Represents a JSON-RPC request with method, parameters, and extensions.\n\nThis is the core structure for all MCP requests, containing:\n- `method`: The name of the method being called\n- `params`: The parameters for the method\n- `extensions`: Additional context data (similar to HTTP headers)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/ElicitationCreateRequestMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/CreateElicitationRequestParam"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
]
|
||||
},
|
||||
"RequestNoParam": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1546,6 +1743,9 @@
|
|||
{
|
||||
"$ref": "#/definitions/ListToolsResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CreateElicitationResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/EmptyObject"
|
||||
}
|
||||
|
|
|
|||
86
crates/rmcp/tests/test_resource_link.rs
Normal file
86
crates/rmcp/tests/test_resource_link.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use rmcp::model::{CallToolResult, Content, RawResource};
|
||||
|
||||
#[test]
|
||||
fn test_resource_link_in_tool_result() {
|
||||
// Test creating a tool result with resource links
|
||||
let resource = RawResource::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),
|
||||
]);
|
||||
|
||||
// Serialize to JSON to verify format
|
||||
let json = serde_json::to_string_pretty(&result).unwrap();
|
||||
println!("Tool result with resource link:\n{}", json);
|
||||
|
||||
// Verify JSON contains expected structure
|
||||
assert!(
|
||||
json.contains("\"type\":\"resource_link\"") || json.contains("\"type\": \"resource_link\"")
|
||||
);
|
||||
assert!(
|
||||
json.contains("\"uri\":\"file:///test/file.txt\"")
|
||||
|| json.contains("\"uri\": \"file:///test/file.txt\"")
|
||||
);
|
||||
assert!(json.contains("\"name\":\"test.txt\"") || json.contains("\"name\": \"test.txt\""));
|
||||
|
||||
// Test deserialization
|
||||
let deserialized: CallToolResult = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.content.len(), 2);
|
||||
|
||||
// Check the text content
|
||||
assert!(deserialized.content[0].as_text().is_some());
|
||||
|
||||
// Check the resource link
|
||||
let resource_link = deserialized.content[1]
|
||||
.as_resource_link()
|
||||
.expect("Expected resource link in content[1]");
|
||||
assert_eq!(resource_link.uri, "file:///test/file.txt");
|
||||
assert_eq!(resource_link.name, "test.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 result = CallToolResult::success(vec![Content::resource_link(resource)]);
|
||||
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
let deserialized: CallToolResult = serde_json::from_str(&json).unwrap();
|
||||
|
||||
let resource_link = deserialized.content[0]
|
||||
.as_resource_link()
|
||||
.expect("Expected resource link");
|
||||
assert_eq!(resource_link.uri, "https://example.com/data.json");
|
||||
assert_eq!(resource_link.name, "API Data");
|
||||
assert_eq!(
|
||||
resource_link.description,
|
||||
Some("JSON data from external API".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
resource_link.mime_type,
|
||||
Some("application/json".to_string())
|
||||
);
|
||||
assert_eq!(resource_link.size, Some(1024));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_content_types() {
|
||||
// Test that resource links can be mixed with other content types
|
||||
let resource = RawResource::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"),
|
||||
]);
|
||||
|
||||
assert_eq!(result.content.len(), 3);
|
||||
assert!(result.content[0].as_text().is_some());
|
||||
assert!(result.content[1].as_resource_link().is_some());
|
||||
assert!(result.content[2].as_resource().is_some());
|
||||
}
|
||||
131
crates/rmcp/tests/test_resource_link_integration.rs
Normal file
131
crates/rmcp/tests/test_resource_link_integration.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/// Integration tests for resource_link support in both tools and prompts
|
||||
use rmcp::model::{
|
||||
AnnotateAble, CallToolResult, Content, PromptMessage, PromptMessageContent, PromptMessageRole,
|
||||
RawResource, Resource,
|
||||
};
|
||||
|
||||
#[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();
|
||||
|
||||
// Test 1: Tool returning a resource link
|
||||
let tool_result = CallToolResult::success(vec![
|
||||
Content::text("Found shared data"),
|
||||
Content::resource_link(resource.clone()),
|
||||
]);
|
||||
|
||||
let tool_json = serde_json::to_string(&tool_result).unwrap();
|
||||
assert!(tool_json.contains("\"type\":\"resource_link\""));
|
||||
|
||||
// Test 2: Prompt returning a resource link
|
||||
let prompt_message =
|
||||
PromptMessage::new_resource_link(PromptMessageRole::Assistant, resource_annotated.clone());
|
||||
|
||||
let prompt_json = serde_json::to_string(&prompt_message).unwrap();
|
||||
assert!(prompt_json.contains("\"type\":\"resource_link\""));
|
||||
|
||||
// Test 3: Verify both serialize to the same resource link structure
|
||||
let tool_content = &tool_result.content[1];
|
||||
let prompt_content = &prompt_message.content;
|
||||
|
||||
// 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!(
|
||||
tool_resource_json.get("type").unwrap(),
|
||||
prompt_resource_json.get("type").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
tool_resource_json.get("uri").unwrap(),
|
||||
prompt_resource_json.get("uri").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
tool_resource_json.get("name").unwrap(),
|
||||
prompt_resource_json.get("name").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_link_roundtrip() {
|
||||
// Test that resource links can be serialized and deserialized correctly
|
||||
// in both tool results and prompt messages
|
||||
|
||||
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_json = serde_json::to_string(&tool_result).unwrap();
|
||||
let tool_deserialized: CallToolResult = serde_json::from_str(&tool_json).unwrap();
|
||||
|
||||
if let Some(resource_link) = tool_deserialized.content[0].as_resource_link() {
|
||||
assert_eq!(resource_link.uri, "https://api.example.com/resource");
|
||||
assert_eq!(resource_link.name, "API Resource");
|
||||
assert_eq!(
|
||||
resource_link.description,
|
||||
Some("External API resource".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
resource_link.mime_type,
|
||||
Some("application/json".to_string())
|
||||
);
|
||||
assert_eq!(resource_link.size, Some(2048));
|
||||
} else {
|
||||
panic!("Expected resource link in tool result");
|
||||
}
|
||||
|
||||
// Test with prompt message
|
||||
let prompt_message = PromptMessage {
|
||||
role: PromptMessageRole::User,
|
||||
content: PromptMessageContent::resource_link(resource.no_annotation()),
|
||||
};
|
||||
|
||||
let prompt_json = serde_json::to_string(&prompt_message).unwrap();
|
||||
let prompt_deserialized: PromptMessage = serde_json::from_str(&prompt_json).unwrap();
|
||||
|
||||
if let PromptMessageContent::ResourceLink { link } = prompt_deserialized.content {
|
||||
assert_eq!(link.uri, "https://api.example.com/resource");
|
||||
assert_eq!(link.name, "API Resource");
|
||||
assert_eq!(link.description, Some("External API resource".to_string()));
|
||||
assert_eq!(link.mime_type, Some("application/json".to_string()));
|
||||
assert_eq!(link.size, Some(2048));
|
||||
} else {
|
||||
panic!("Expected resource link in prompt message");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 = 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"),
|
||||
]);
|
||||
|
||||
assert_eq!(tool_result.content.len(), 4);
|
||||
assert!(tool_result.content[0].as_text().is_some());
|
||||
assert!(tool_result.content[1].as_resource_link().is_some());
|
||||
assert!(tool_result.content[2].as_resource_link().is_some());
|
||||
assert!(tool_result.content[3].as_resource().is_some());
|
||||
|
||||
// Verify serialization includes all types
|
||||
let json = serde_json::to_string(&tool_result).unwrap();
|
||||
assert!(json.contains("\"type\":\"text\""));
|
||||
assert!(json.contains("\"type\":\"resource_link\""));
|
||||
assert!(json.contains("\"type\":\"resource\""));
|
||||
}
|
||||
Loading…
Reference in a new issue