Skip to main content

soma_mcp_server/
conformance.rs

1//! Official MCP conformance-suite reference fixtures.
2//!
3//! These are generic, spec-defined test scenarios (simple text, image,
4//! audio, embedded-resource, and mixed content blocks; error handling; JSON
5//! Schema 2020-12 features) with no product concepts attached. A host server
6//! typically hides these behind its own opt-in flag so real deployments do
7//! not advertise test-only tools, resources, or prompts.
8
9use 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;