From 71978a90331bfd49915f611cf033a64cfd3f6030 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Tue, 26 Aug 2025 12:45:21 -0400 Subject: [PATCH 1/3] feat: add resource_link support to tools and prompts --- crates/rmcp/src/model/content.rs | 64 +- crates/rmcp/src/model/prompt.rs | 59 +- .../client_json_rpc_message_schema.json | 58 + ...lient_json_rpc_message_schema_current.json | 1414 +++++++++++++++++ .../server_json_rpc_message_schema.json | 114 ++ ...erver_json_rpc_message_schema_current.json | 210 ++- crates/rmcp/tests/test_resource_link.rs | 87 + .../tests/test_resource_link_integration.rs | 132 ++ 8 files changed, 2131 insertions(+), 7 deletions(-) create mode 100644 crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json create mode 100644 crates/rmcp/tests/test_resource_link.rs create mode 100644 crates/rmcp/tests/test_resource_link_integration.rs diff --git a/crates/rmcp/src/model/content.rs b/crates/rmcp/src/model/content.rs index 964f6043..52d34523 100644 --- a/crates/rmcp/src/model/content.rs +++ b/crates/rmcp/src/model/content.rs @@ -51,13 +51,14 @@ pub struct RawAudioContent { pub type AudioContent = Annotated; #[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; @@ -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(json: S) -> Result { 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"); + } + } } diff --git a/crates/rmcp/src/model/prompt.rs b/crates/rmcp/src/model/prompt.rs index 51293cd4..138a7442 100644 --- a/crates/rmcp/src/model/prompt.rs +++ b/crates/rmcp/src/model/prompt.rs @@ -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) -> 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"); + } + } } diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json index d11c105c..ce59d36f 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json @@ -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": { diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json new file mode 100644 index 00000000..ce59d36f --- /dev/null +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json @@ -0,0 +1,1414 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "JsonRpcMessage", + "description": "Represents any JSON-RPC message that can be sent or received.\n\nThis enum covers all possible message types in the JSON-RPC protocol:\nindividual requests/responses, notifications, batch operations, and errors.\nIt serves as the top-level message container for MCP communication.", + "anyOf": [ + { + "description": "A single request expecting a response", + "allOf": [ + { + "$ref": "#/definitions/JsonRpcRequest" + } + ] + }, + { + "description": "A response to a previous request", + "allOf": [ + { + "$ref": "#/definitions/JsonRpcResponse" + } + ] + }, + { + "description": "A one-way notification (no response expected)", + "allOf": [ + { + "$ref": "#/definitions/JsonRpcNotification" + } + ] + }, + { + "description": "Multiple requests sent together", + "type": "array", + "items": { + "$ref": "#/definitions/JsonRpcBatchRequestItem" + } + }, + { + "description": "Multiple responses sent together", + "type": "array", + "items": { + "$ref": "#/definitions/JsonRpcBatchResponseItem" + } + }, + { + "description": "An error response", + "allOf": [ + { + "$ref": "#/definitions/JsonRpcError" + } + ] + } + ], + "definitions": { + "Annotated": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + } + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + } + }, + "allOf": [ + { + "$ref": "#/definitions/RawTextContent" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "image" + } + }, + "allOf": [ + { + "$ref": "#/definitions/RawImageContent" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource" + } + }, + "allOf": [ + { + "$ref": "#/definitions/RawEmbeddedResource" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "audio" + } + }, + "allOf": [ + { + "$ref": "#/definitions/Annotated2" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_link" + } + }, + "allOf": [ + { + "$ref": "#/definitions/RawResource" + } + ], + "required": [ + "type" + ] + } + ] + }, + "Annotated2": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/definitions/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "data", + "mimeType" + ] + }, + "Annotations": { + "type": "object", + "properties": { + "audience": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Role" + } + }, + "priority": { + "type": [ + "number", + "null" + ], + "format": "float" + }, + "timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + } + }, + "ArgumentInfo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "name", + "value" + ] + }, + "CallToolRequestMethod": { + "type": "string", + "format": "const", + "const": "tools/call" + }, + "CallToolRequestParam": { + "description": "Parameters for calling a tool provided by an MCP server.\n\nContains the tool name and optional arguments needed to execute\nthe tool operation.", + "type": "object", + "properties": { + "arguments": { + "description": "Arguments to pass to the tool (must match the tool's input schema)", + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "name": { + "description": "The name of the tool to call", + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "CancelledNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/cancelled" + }, + "CancelledNotificationParam": { + "type": "object", + "properties": { + "reason": { + "type": [ + "string", + "null" + ] + }, + "requestId": { + "$ref": "#/definitions/NumberOrString" + } + }, + "required": [ + "requestId" + ] + }, + "ClientCapabilities": { + "title": "Builder", + "description": "```rust\n# use rmcp::model::ClientCapabilities;\nlet cap = ClientCapabilities::builder()\n .enable_experimental()\n .enable_roots()\n .enable_roots_list_changed()\n .build();\n```", + "type": "object", + "properties": { + "elicitation": { + "description": "Capability to handle elicitation requests from servers for interactive user input", + "anyOf": [ + { + "$ref": "#/definitions/ElicitationCapability" + }, + { + "type": "null" + } + ] + }, + "experimental": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + }, + "roots": { + "anyOf": [ + { + "$ref": "#/definitions/RootsCapabilities" + }, + { + "type": "null" + } + ] + }, + "sampling": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + } + }, + "ClientResult": { + "anyOf": [ + { + "$ref": "#/definitions/CreateMessageResult" + }, + { + "$ref": "#/definitions/ListRootsResult" + }, + { + "$ref": "#/definitions/CreateElicitationResult" + }, + { + "$ref": "#/definitions/EmptyObject" + } + ] + }, + "CompleteRequestMethod": { + "type": "string", + "format": "const", + "const": "completion/complete" + }, + "CompleteRequestParam": { + "type": "object", + "properties": { + "argument": { + "$ref": "#/definitions/ArgumentInfo" + }, + "ref": { + "$ref": "#/definitions/Reference" + } + }, + "required": [ + "ref", + "argument" + ] + }, + "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" + ] + }, + "CreateMessageResult": { + "description": "The result of a sampling/createMessage request containing the generated response.\n\nThis structure contains the generated message along with metadata about\nhow the generation was performed and why it stopped.", + "type": "object", + "properties": { + "content": { + "description": "The actual content of the message (text, image, etc.)", + "allOf": [ + { + "$ref": "#/definitions/Annotated" + } + ] + }, + "model": { + "description": "The identifier of the model that generated the response", + "type": "string" + }, + "role": { + "description": "The role of the message sender (User or Assistant)", + "allOf": [ + { + "$ref": "#/definitions/Role" + } + ] + }, + "stopReason": { + "description": "The reason why generation stopped (e.g., \"endTurn\", \"maxTokens\")", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "model", + "role", + "content" + ] + }, + "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" + } + ] + }, + "ElicitationCapability": { + "description": "Capability for handling elicitation requests from servers.\n\nElicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.", + "type": "object", + "properties": { + "schemaValidation": { + "description": "Whether the client supports JSON Schema validation for elicitation responses.\nWhen true, the client will validate user input against the requested_schema\nbefore sending the response back to the server.", + "type": [ + "boolean", + "null" + ] + } + } + }, + "EmptyObject": { + "description": "This is commonly used for representing empty objects in MCP messages.\n\nwithout returning any specific data.", + "type": "object" + }, + "ErrorCode": { + "description": "Standard JSON-RPC error codes used throughout the MCP protocol.\n\nThese codes follow the JSON-RPC 2.0 specification and provide\nstandardized error reporting across all MCP implementations.", + "type": "integer", + "format": "int32" + }, + "ErrorData": { + "description": "Error information for JSON-RPC error responses.\n\nThis structure follows the JSON-RPC 2.0 specification for error reporting,\nproviding a standardized way to communicate errors between clients and servers.", + "type": "object", + "properties": { + "code": { + "description": "The error type that occurred (using standard JSON-RPC error codes)", + "allOf": [ + { + "$ref": "#/definitions/ErrorCode" + } + ] + }, + "data": { + "description": "Additional information about the error. The value of this member is defined by the\nsender (e.g. detailed error information, nested errors etc.)." + }, + "message": { + "description": "A short description of the error. The message SHOULD be limited to a concise single sentence.", + "type": "string" + } + }, + "required": [ + "code", + "message" + ] + }, + "GetPromptRequestMethod": { + "type": "string", + "format": "const", + "const": "prompts/get" + }, + "GetPromptRequestParam": { + "description": "Parameters for retrieving a specific prompt", + "type": "object", + "properties": { + "arguments": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "Implementation": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "name", + "version" + ] + }, + "InitializeRequestParam": { + "description": "Parameters sent by a client when initializing a connection to an MCP server.\n\nThis contains the client's protocol version, capabilities, and implementation\ninformation, allowing the server to understand what the client supports.", + "type": "object", + "properties": { + "capabilities": { + "description": "The capabilities this client supports (sampling, roots, etc.)", + "allOf": [ + { + "$ref": "#/definitions/ClientCapabilities" + } + ] + }, + "clientInfo": { + "description": "Information about the client implementation", + "allOf": [ + { + "$ref": "#/definitions/Implementation" + } + ] + }, + "protocolVersion": { + "description": "The MCP protocol version this client supports", + "allOf": [ + { + "$ref": "#/definitions/ProtocolVersion" + } + ] + } + }, + "required": [ + "protocolVersion", + "capabilities", + "clientInfo" + ] + }, + "InitializeResultMethod": { + "type": "string", + "format": "const", + "const": "initialize" + }, + "InitializedNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/initialized" + }, + "JsonRpcBatchRequestItem": { + "anyOf": [ + { + "$ref": "#/definitions/JsonRpcRequest" + }, + { + "$ref": "#/definitions/JsonRpcNotification" + } + ] + }, + "JsonRpcBatchResponseItem": { + "anyOf": [ + { + "$ref": "#/definitions/JsonRpcResponse" + }, + { + "$ref": "#/definitions/JsonRpcError" + } + ] + }, + "JsonRpcError": { + "type": "object", + "properties": { + "error": { + "$ref": "#/definitions/ErrorData" + }, + "id": { + "$ref": "#/definitions/NumberOrString" + }, + "jsonrpc": { + "$ref": "#/definitions/JsonRpcVersion2_0" + } + }, + "required": [ + "jsonrpc", + "id", + "error" + ] + }, + "JsonRpcNotification": { + "type": "object", + "properties": { + "jsonrpc": { + "$ref": "#/definitions/JsonRpcVersion2_0" + } + }, + "anyOf": [ + { + "$ref": "#/definitions/Notification" + }, + { + "$ref": "#/definitions/Notification2" + }, + { + "$ref": "#/definitions/NotificationNoParam" + }, + { + "$ref": "#/definitions/NotificationNoParam2" + } + ], + "required": [ + "jsonrpc" + ] + }, + "JsonRpcRequest": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/NumberOrString" + }, + "jsonrpc": { + "$ref": "#/definitions/JsonRpcVersion2_0" + } + }, + "anyOf": [ + { + "$ref": "#/definitions/RequestNoParam" + }, + { + "$ref": "#/definitions/Request" + }, + { + "$ref": "#/definitions/Request2" + }, + { + "$ref": "#/definitions/Request3" + }, + { + "$ref": "#/definitions/Request4" + }, + { + "$ref": "#/definitions/RequestOptionalParam" + }, + { + "$ref": "#/definitions/RequestOptionalParam2" + }, + { + "$ref": "#/definitions/RequestOptionalParam3" + }, + { + "$ref": "#/definitions/Request5" + }, + { + "$ref": "#/definitions/Request6" + }, + { + "$ref": "#/definitions/Request7" + }, + { + "$ref": "#/definitions/Request8" + }, + { + "$ref": "#/definitions/RequestOptionalParam4" + } + ], + "required": [ + "jsonrpc", + "id" + ] + }, + "JsonRpcResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/NumberOrString" + }, + "jsonrpc": { + "$ref": "#/definitions/JsonRpcVersion2_0" + }, + "result": { + "$ref": "#/definitions/ClientResult" + } + }, + "required": [ + "jsonrpc", + "id", + "result" + ] + }, + "JsonRpcVersion2_0": { + "type": "string", + "format": "const", + "const": "2.0" + }, + "ListPromptsRequestMethod": { + "type": "string", + "format": "const", + "const": "prompts/list" + }, + "ListResourceTemplatesRequestMethod": { + "type": "string", + "format": "const", + "const": "resources/templates/list" + }, + "ListResourcesRequestMethod": { + "type": "string", + "format": "const", + "const": "resources/list" + }, + "ListRootsResult": { + "type": "object", + "properties": { + "roots": { + "type": "array", + "items": { + "$ref": "#/definitions/Root" + } + } + }, + "required": [ + "roots" + ] + }, + "ListToolsRequestMethod": { + "type": "string", + "format": "const", + "const": "tools/list" + }, + "LoggingLevel": { + "description": "Logging levels supported by the MCP protocol", + "type": "string", + "enum": [ + "debug", + "info", + "notice", + "warning", + "error", + "critical", + "alert", + "emergency" + ] + }, + "Notification": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/CancelledNotificationMethod" + }, + "params": { + "$ref": "#/definitions/CancelledNotificationParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Notification2": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ProgressNotificationMethod" + }, + "params": { + "$ref": "#/definitions/ProgressNotificationParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "NotificationNoParam": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/InitializedNotificationMethod" + } + }, + "required": [ + "method" + ] + }, + "NotificationNoParam2": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/RootsListChangedNotificationMethod" + } + }, + "required": [ + "method" + ] + }, + "NumberOrString": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "PaginatedRequestParam": { + "type": "object", + "properties": { + "cursor": { + "type": [ + "string", + "null" + ] + } + } + }, + "PingRequestMethod": { + "type": "string", + "format": "const", + "const": "ping" + }, + "ProgressNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/progress" + }, + "ProgressNotificationParam": { + "type": "object", + "properties": { + "message": { + "description": "An optional message describing the current progress.", + "type": [ + "string", + "null" + ] + }, + "progress": { + "description": "The progress thus far. This should increase every time progress is made, even if the total is unknown.", + "type": "number", + "format": "double" + }, + "progressToken": { + "$ref": "#/definitions/ProgressToken" + }, + "total": { + "description": "Total number of items to process (or total progress required), if known", + "type": [ + "number", + "null" + ], + "format": "double" + } + }, + "required": [ + "progressToken", + "progress" + ] + }, + "ProgressToken": { + "description": "A token used to track the progress of long-running operations.\n\nProgress tokens allow clients and servers to associate progress notifications\nwith specific requests, enabling real-time updates on operation status.", + "allOf": [ + { + "$ref": "#/definitions/NumberOrString" + } + ] + }, + "PromptReference": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "ProtocolVersion": { + "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" + }, + "RawEmbeddedResource": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/definitions/ResourceContents" + } + }, + "required": [ + "resource" + ] + }, + "RawImageContent": { + "type": "object", + "properties": { + "data": { + "description": "The base64-encoded image", + "type": "string" + }, + "mimeType": { + "type": "string" + } + }, + "required": [ + "data", + "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": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ] + }, + "ReadResourceRequestMethod": { + "type": "string", + "format": "const", + "const": "resources/read" + }, + "ReadResourceRequestParam": { + "description": "Parameters for reading a specific resource", + "type": "object", + "properties": { + "uri": { + "description": "The URI of the resource to read", + "type": "string" + } + }, + "required": [ + "uri" + ] + }, + "Reference": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "ref/resource" + } + }, + "allOf": [ + { + "$ref": "#/definitions/ResourceReference" + } + ], + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "ref/prompt" + } + }, + "allOf": [ + { + "$ref": "#/definitions/PromptReference" + } + ], + "required": [ + "type" + ] + } + ] + }, + "Request": { + "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/InitializeResultMethod" + }, + "params": { + "$ref": "#/definitions/InitializeRequestParam" + } + }, + "required": [ + "method", + "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/CompleteRequestMethod" + }, + "params": { + "$ref": "#/definitions/CompleteRequestParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Request3": { + "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/SetLevelRequestMethod" + }, + "params": { + "$ref": "#/definitions/SetLevelRequestParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Request4": { + "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/GetPromptRequestMethod" + }, + "params": { + "$ref": "#/definitions/GetPromptRequestParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Request5": { + "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/ReadResourceRequestMethod" + }, + "params": { + "$ref": "#/definitions/ReadResourceRequestParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Request6": { + "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/SubscribeRequestMethod" + }, + "params": { + "$ref": "#/definitions/SubscribeRequestParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Request7": { + "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/UnsubscribeRequestMethod" + }, + "params": { + "$ref": "#/definitions/UnsubscribeRequestParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "Request8": { + "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/CallToolRequestMethod" + }, + "params": { + "$ref": "#/definitions/CallToolRequestParam" + } + }, + "required": [ + "method", + "params" + ] + }, + "RequestNoParam": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/PingRequestMethod" + } + }, + "required": [ + "method" + ] + }, + "RequestOptionalParam": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ListPromptsRequestMethod" + }, + "params": { + "anyOf": [ + { + "$ref": "#/definitions/PaginatedRequestParam" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "method" + ] + }, + "RequestOptionalParam2": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ListResourcesRequestMethod" + }, + "params": { + "anyOf": [ + { + "$ref": "#/definitions/PaginatedRequestParam" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "method" + ] + }, + "RequestOptionalParam3": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ListResourceTemplatesRequestMethod" + }, + "params": { + "anyOf": [ + { + "$ref": "#/definitions/PaginatedRequestParam" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "method" + ] + }, + "RequestOptionalParam4": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ListToolsRequestMethod" + }, + "params": { + "anyOf": [ + { + "$ref": "#/definitions/PaginatedRequestParam" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "method" + ] + }, + "ResourceContents": { + "anyOf": [ + { + "type": "object", + "properties": { + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "text": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "uri", + "text" + ] + }, + { + "type": "object", + "properties": { + "blob": { + "type": "string" + }, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "uri", + "blob" + ] + } + ] + }, + "ResourceReference": { + "type": "object", + "properties": { + "uri": { + "type": "string" + } + }, + "required": [ + "uri" + ] + }, + "Role": { + "description": "Represents the role of a participant in a conversation or message exchange.\n\nUsed in sampling and chat contexts to distinguish between different\ntypes of message senders in the conversation flow.", + "oneOf": [ + { + "description": "A human user or client making a request", + "type": "string", + "const": "user" + }, + { + "description": "An AI assistant or server providing a response", + "type": "string", + "const": "assistant" + } + ] + }, + "Root": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "uri" + ] + }, + "RootsCapabilities": { + "type": "object", + "properties": { + "listChanged": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "RootsListChangedNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/roots/list_changed" + }, + "SetLevelRequestMethod": { + "type": "string", + "format": "const", + "const": "logging/setLevel" + }, + "SetLevelRequestParam": { + "description": "Parameters for setting the logging level", + "type": "object", + "properties": { + "level": { + "description": "The desired logging level", + "allOf": [ + { + "$ref": "#/definitions/LoggingLevel" + } + ] + } + }, + "required": [ + "level" + ] + }, + "SubscribeRequestMethod": { + "type": "string", + "format": "const", + "const": "resources/subscribe" + }, + "SubscribeRequestParam": { + "description": "Parameters for subscribing to resource updates", + "type": "object", + "properties": { + "uri": { + "description": "The URI of the resource to subscribe to", + "type": "string" + } + }, + "required": [ + "uri" + ] + }, + "UnsubscribeRequestMethod": { + "type": "string", + "format": "const", + "const": "resources/unsubscribe" + }, + "UnsubscribeRequestParam": { + "description": "Parameters for unsubscribing from resource updates", + "type": "object", + "properties": { + "uri": { + "description": "The URI of the resource to unsubscribe from", + "type": "string" + } + }, + "required": [ + "uri" + ] + } + } +} \ No newline at end of file diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index c0b441c4..4736b5ba 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -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": { diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index a0fa15e2..4736b5ba 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -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" } diff --git a/crates/rmcp/tests/test_resource_link.rs b/crates/rmcp/tests/test_resource_link.rs new file mode 100644 index 00000000..77249415 --- /dev/null +++ b/crates/rmcp/tests/test_resource_link.rs @@ -0,0 +1,87 @@ +use rmcp::model::{CallToolResult, Content, RawResource}; +use serde_json; + +#[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()); +} diff --git a/crates/rmcp/tests/test_resource_link_integration.rs b/crates/rmcp/tests/test_resource_link_integration.rs new file mode 100644 index 00000000..895c54cf --- /dev/null +++ b/crates/rmcp/tests/test_resource_link_integration.rs @@ -0,0 +1,132 @@ +/// Integration tests for resource_link support in both tools and prompts +use rmcp::model::{ + AnnotateAble, CallToolResult, Content, PromptMessage, PromptMessageContent, PromptMessageRole, + RawResource, Resource, +}; +use serde_json; + +#[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\"")); +} From 5310d5e7270290957817051600018d77942eb42b Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Tue, 26 Aug 2025 14:19:22 -0400 Subject: [PATCH 2/3] chore: remove unused serde_json import from test_resource_link_integration.rs --- crates/rmcp/tests/test_resource_link_integration.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/rmcp/tests/test_resource_link_integration.rs b/crates/rmcp/tests/test_resource_link_integration.rs index 895c54cf..ab663525 100644 --- a/crates/rmcp/tests/test_resource_link_integration.rs +++ b/crates/rmcp/tests/test_resource_link_integration.rs @@ -3,7 +3,6 @@ use rmcp::model::{ AnnotateAble, CallToolResult, Content, PromptMessage, PromptMessageContent, PromptMessageRole, RawResource, Resource, }; -use serde_json; #[test] fn test_tool_and_prompt_resource_link_compatibility() { From 68fe486c22bd72aaf950452a40c853733eed2430 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Tue, 26 Aug 2025 14:40:37 -0400 Subject: [PATCH 3/3] chore: remove unused serde_json import from test_resource_link.rs --- crates/rmcp/tests/test_resource_link.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/rmcp/tests/test_resource_link.rs b/crates/rmcp/tests/test_resource_link.rs index 77249415..685a645c 100644 --- a/crates/rmcp/tests/test_resource_link.rs +++ b/crates/rmcp/tests/test_resource_link.rs @@ -1,5 +1,4 @@ use rmcp::model::{CallToolResult, Content, RawResource}; -use serde_json; #[test] fn test_resource_link_in_tool_result() {