1use std::{borrow::Cow, sync::Arc};
10
11use rmcp::model::{
12 AudioContent, CallToolResult, ContentBlock, GetPromptRequestParams, GetPromptResult,
13 ImageContent, Prompt, PromptArgument, PromptMessage, ReadResourceResult, Resource,
14 ResourceContents, ResourceTemplate, Role, Tool,
15};
16use serde_json::{Map, Value, json};
17
18const PNG_1X1_RED: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADUlEQVR42mP8z8BQDwAFgwJ/lZc47wAAAABJRU5ErkJggg==";
19const WAV_SILENCE: &str = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQAAAAA=";
20
21pub fn tool_definitions() -> Vec<Tool> {
22 [
23 ("test_simple_text", "Returns a simple text content block."),
24 (
25 "test_image_content",
26 "Returns a minimal PNG image content block.",
27 ),
28 (
29 "test_audio_content",
30 "Returns a minimal WAV audio content block.",
31 ),
32 (
33 "test_embedded_resource",
34 "Returns an embedded text resource content block.",
35 ),
36 (
37 "test_multiple_content_types",
38 "Returns text, image, and embedded resource content together.",
39 ),
40 (
41 "test_error_handling",
42 "Returns an MCP tool result with isError=true.",
43 ),
44 (
45 "json_schema_2020_12_tool",
46 "Tool with JSON Schema 2020-12 features.",
47 ),
48 ]
49 .into_iter()
50 .map(|(name, description)| {
51 let schema = if name == "json_schema_2020_12_tool" {
52 json_schema_2020_12_input_schema()
53 } else {
54 empty_input_schema()
55 };
56 Tool::new_with_raw(
57 Cow::Owned(name.to_string()),
58 Some(Cow::Owned(description.to_string())),
59 Arc::new(schema),
60 )
61 })
62 .collect()
63}
64
65pub fn call_tool(name: &str) -> Option<CallToolResult> {
66 let result = match name {
67 "test_simple_text" => CallToolResult::success(vec![ContentBlock::text(
68 "This is a simple text response for testing.",
69 )]),
70 "test_image_content" => {
71 CallToolResult::success(vec![ContentBlock::image(PNG_1X1_RED, "image/png")])
72 }
73 "test_audio_content" => CallToolResult::success(vec![ContentBlock::Audio(
74 AudioContent::new(WAV_SILENCE, "audio/wav"),
75 )]),
76 "test_embedded_resource" => CallToolResult::success(vec![ContentBlock::resource(
77 ResourceContents::text(
78 "This is an embedded resource content.",
79 "test://embedded-resource",
80 )
81 .with_mime_type("text/plain"),
82 )]),
83 "test_multiple_content_types" => CallToolResult::success(vec![
84 ContentBlock::text("Multiple content types test:"),
85 ContentBlock::image(PNG_1X1_RED, "image/png"),
86 ContentBlock::resource(
87 ResourceContents::text(
88 r#"{"test":"data","value":123}"#,
89 "test://mixed-content-resource",
90 )
91 .with_mime_type("application/json"),
92 ),
93 ]),
94 "test_error_handling" => CallToolResult::error(vec![ContentBlock::text(
95 "This tool intentionally returns an error for testing",
96 )]),
97 _ => return None,
98 };
99 Some(result)
100}
101
102pub fn resources() -> Vec<Resource> {
103 vec![
104 Resource::new("test://static-text", "static text fixture")
105 .with_description("MCP conformance text resource fixture")
106 .with_mime_type("text/plain"),
107 Resource::new("test://static-binary", "static binary fixture")
108 .with_description("MCP conformance binary resource fixture")
109 .with_mime_type("image/png"),
110 ]
111}
112
113pub fn resource_templates() -> Vec<ResourceTemplate> {
114 vec![
115 ResourceTemplate::new("test://template/{id}/data", "template data by id")
116 .with_description("MCP conformance templated JSON resource fixture")
117 .with_mime_type("application/json"),
118 ]
119}
120
121pub fn read_resource(uri: &str) -> Option<ReadResourceResult> {
122 let contents = match uri {
123 "test://static-text" => vec![
124 ResourceContents::text(
125 "This is the content of the static text resource.",
126 "test://static-text",
127 )
128 .with_mime_type("text/plain"),
129 ],
130 "test://static-binary" => {
131 vec![
132 ResourceContents::blob(PNG_1X1_RED, "test://static-binary")
133 .with_mime_type("image/png"),
134 ]
135 }
136 "test://template/123/data" => vec![
137 ResourceContents::text(
138 r#"{"id":"123","templateTest":true,"data":"Data for ID: 123"}"#,
139 "test://template/123/data",
140 )
141 .with_mime_type("application/json"),
142 ],
143 _ => return None,
144 };
145 Some(ReadResourceResult::new(contents))
146}
147
148pub fn prompts() -> Vec<Prompt> {
149 vec![
150 Prompt::new(
151 "test_simple_prompt",
152 Some("MCP conformance simple prompt fixture"),
153 None,
154 ),
155 Prompt::new(
156 "test_prompt_with_arguments",
157 Some("MCP conformance argument-substitution prompt fixture"),
158 Some(vec![
159 PromptArgument::new("arg1")
160 .with_description("First test argument")
161 .with_required(true),
162 PromptArgument::new("arg2")
163 .with_description("Second test argument")
164 .with_required(true),
165 ]),
166 ),
167 Prompt::new(
168 "test_prompt_with_embedded_resource",
169 Some("MCP conformance embedded-resource prompt fixture"),
170 Some(vec![
171 PromptArgument::new("resourceUri")
172 .with_description("URI of the resource to embed")
173 .with_required(true),
174 ]),
175 ),
176 Prompt::new(
177 "test_prompt_with_image",
178 Some("MCP conformance image prompt fixture"),
179 None,
180 ),
181 ]
182}
183
184pub fn get_prompt(request: GetPromptRequestParams) -> Option<GetPromptResult> {
185 let result = match request.name.as_str() {
186 "test_simple_prompt" => GetPromptResult::new(vec![PromptMessage::new_text(
187 Role::User,
188 "This is a simple prompt for testing.",
189 )]),
190 "test_prompt_with_arguments" => {
191 let args = request.arguments.as_ref();
192 let arg1 = prompt_arg(args, "arg1");
193 let arg2 = prompt_arg(args, "arg2");
194 GetPromptResult::new(vec![PromptMessage::new_text(
195 Role::User,
196 format!("Prompt with arguments: arg1='{arg1}', arg2='{arg2}'"),
197 )])
198 }
199 "test_prompt_with_embedded_resource" => {
200 let args = request.arguments.as_ref();
201 let uri = prompt_arg(args, "resourceUri");
202 GetPromptResult::new(vec![
203 PromptMessage::new_resource(
204 Role::User,
205 uri,
206 Some("text/plain".to_string()),
207 Some("Embedded resource content for testing.".to_string()),
208 None,
209 None,
210 None,
211 ),
212 PromptMessage::new_text(Role::User, "Please process the embedded resource above."),
213 ])
214 }
215 "test_prompt_with_image" => GetPromptResult::new(vec![
216 PromptMessage::new(
217 Role::User,
218 ContentBlock::Image(ImageContent::new(PNG_1X1_RED, "image/png")),
219 ),
220 PromptMessage::new_text(Role::User, "Please analyze the image above."),
221 ]),
222 _ => return None,
223 };
224 Some(result)
225}
226
227fn empty_input_schema() -> Map<String, Value> {
228 serde_json::from_value(json!({
229 "type": "object",
230 "properties": {},
231 "additionalProperties": false
232 }))
233 .expect("static conformance input schema must be a JSON object")
234}
235
236fn json_schema_2020_12_input_schema() -> Map<String, Value> {
237 serde_json::from_value(json!({
238 "$schema": "https://json-schema.org/draft/2020-12/schema",
239 "type": "object",
240 "$defs": {
241 "address": {
242 "$anchor": "addressDef",
243 "type": "object",
244 "properties": {
245 "street": { "type": "string" },
246 "city": { "type": "string" }
247 }
248 }
249 },
250 "properties": {
251 "name": { "type": "string" },
252 "address": { "$ref": "#/$defs/address" },
253 "contactMethod": { "type": "string", "enum": ["phone", "email"] },
254 "phone": { "type": "string" },
255 "email": { "type": "string" }
256 },
257 "allOf": [
258 { "anyOf": [{ "required": ["phone"] }, { "required": ["email"] }] }
259 ],
260 "if": {
261 "properties": { "contactMethod": { "const": "phone" } },
262 "required": ["contactMethod"]
263 },
264 "then": { "required": ["phone"] },
265 "else": { "required": ["email"] },
266 "additionalProperties": false
267 }))
268 .expect("static JSON Schema 2020-12 fixture must be a JSON object")
269}
270
271fn prompt_arg(args: Option<&Map<String, Value>>, name: &str) -> String {
272 args.and_then(|m| m.get(name))
273 .and_then(Value::as_str)
274 .unwrap_or_default()
275 .to_string()
276}
277
278#[cfg(test)]
279#[path = "conformance_tests.rs"]
280mod tests;