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