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::{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;