Skip to content

Commit e55e8d1

Browse files
committed
Fix validation and default missing fields
1 parent 356a815 commit e55e8d1

File tree

3 files changed

+166
-49
lines changed

3 files changed

+166
-49
lines changed

Cargo.lock

+99
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/mcp-apollo-server/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ webbrowser = "1.0.4"
3737

3838
[dev-dependencies]
3939
insta = { workspace = true }
40+
rstest = "0.25.0"
4041

4142
[lints]
4243
workspace = true

crates/mcp-apollo-server/src/explorer.rs

+66-49
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ pub struct Explorer {
2121
pub struct Input {
2222
/// The GraphQL document
2323
document: String,
24+
25+
/// Any variables used in the document
2426
variables: String,
27+
28+
/// Headers to be sent with the operation
2529
headers: String,
2630
}
2731

@@ -36,70 +40,42 @@ impl Explorer {
3640
variant,
3741
tool: Tool::new(
3842
EXPLORER_TOOL_NAME,
39-
"Open a GraphQL operation in Apollo Explorer. The input fields must be in the order document, variables, headers. All fields must be present, but if they are not set, set them to a string containing just `{}`",
43+
"Open a GraphQL operation in Apollo Explorer",
4044
schema_from_type!(Input),
4145
),
4246
}
4347
}
4448

4549
fn create_explorer_url(&self, input: &Value) -> String {
46-
let compressed = lz_str::compress_to_encoded_uri_component(input.to_string().as_str());
47-
format!(
48-
"https://studio.apollographql.com/graph/{graph_id}/variant/{variant}/explorer?explorerURLState={compressed}",
49-
graph_id = self.graph_id,
50-
variant = self.variant
51-
)
52-
}
53-
54-
pub async fn execute(&self, input: Value) -> Result<CallToolResult, McpError> {
55-
// Validate field order: must be ["document", "variables", "headers"]
56-
if let Value::Object(map) = &input {
57-
let mut keys = map.keys();
58-
if !(keys.next() == Some(&"document".to_string())
59-
&& keys.next() == Some(&"variables".to_string())
60-
&& keys.next() == Some(&"headers".to_string())
61-
&& keys.next().is_none())
62-
{
63-
return Err(McpError::new(
64-
ErrorCode::INVALID_PARAMS,
65-
"Input fields must be in order: document, variables, headers",
66-
None,
67-
));
68-
}
69-
} else {
70-
return Err(McpError::new(
71-
ErrorCode::INVALID_PARAMS,
72-
"Input must be a JSON object",
73-
None,
74-
));
75-
}
50+
let mut input = input.clone();
7651

7752
let document = input.get("document").and_then(|v| v.as_str());
78-
let variables = input.get("variables").and_then(|v| v.as_str());
79-
let headers = input.get("headers").and_then(|v| v.as_str());
80-
8153
if document.is_none() || document == Some("") {
82-
return Err(McpError::new(
83-
ErrorCode::INVALID_PARAMS,
84-
"Missing or empty 'document' field in input",
85-
None,
86-
));
54+
if let Some(obj) = input.as_object_mut() {
55+
obj.insert("document".to_string(), Value::String("{}".to_string()));
56+
}
8757
}
58+
let variables = input.get("variables").and_then(|v| v.as_str());
8859
if variables.is_none() || variables == Some("") {
89-
return Err(McpError::new(
90-
ErrorCode::INVALID_PARAMS,
91-
"Missing or empty 'variables' field in input",
92-
None,
93-
));
60+
if let Some(obj) = input.as_object_mut() {
61+
obj.insert("variables".to_string(), Value::String("{}".to_string()));
62+
}
9463
}
64+
let headers = input.get("headers").and_then(|v| v.as_str());
9565
if headers.is_none() || headers == Some("") {
96-
return Err(McpError::new(
97-
ErrorCode::INVALID_PARAMS,
98-
"Missing or empty 'headers' field in input",
99-
None,
100-
));
66+
if let Some(obj) = input.as_object_mut() {
67+
obj.insert("headers".to_string(), Value::String("{}".to_string()));
68+
}
10169
}
70+
let compressed = lz_str::compress_to_encoded_uri_component(input.to_string().as_str());
71+
format!(
72+
"https://studio.apollographql.com/graph/{graph_id}/variant/{variant}/explorer?explorerURLState={compressed}",
73+
graph_id = self.graph_id,
74+
variant = self.variant
75+
)
76+
}
10277

78+
pub async fn execute(&self, input: Value) -> Result<CallToolResult, McpError> {
10379
let url = self.create_explorer_url(&input);
10480
info!(
10581
"Opening Apollo Explorer URL: {} for input operation: {}",
@@ -120,6 +96,7 @@ mod tests {
12096
use super::*;
12197
use insta::assert_snapshot;
12298
use rmcp::serde_json::json;
99+
use rstest::rstest;
123100

124101
#[test]
125102
fn test_create_explorer_url() {
@@ -136,4 +113,44 @@ mod tests {
136113
@"https://studio.apollographql.com/graph/mcp-example/variant/mcp/explorer?explorerURLState=N4IgJg9gxgrgtgUwHYBcQC4QEcYIE4CeABAOIIoDqCAhigBb4CCANvigM4AUAJOyrQnREAyijwBLJAHMAhAEoiwADpIiRaqzwdOfAUN78UCBctVqi7BADd84lARXmiYBOygSADinEQkj85J8eDBQ3r7+AL4qESAANCAM1C547BggwDHxVtQS1ABGrKmYyiC6RkoYRBUAwowVMRFAA"
137114
);
138115
}
116+
117+
#[tokio::test]
118+
#[rstest]
119+
#[case(json!({
120+
"variables": "{\"state\": \"CA\"}",
121+
"headers": "{}"
122+
}), "document")]
123+
#[case(json!({
124+
"document": "query GetWeatherAlerts($state: String!) {\n alerts(state: $state) {\n severity\n description\n instruction\n }\n}",
125+
"headers": "{}"
126+
}), "variables")]
127+
#[case(json!({
128+
"document": "query GetWeatherAlerts($state: String!) {\n alerts(state: $state) {\n severity\n description\n instruction\n }\n}",
129+
"variables": "{\"state\": \"CA\"}"
130+
}), "headers")]
131+
async fn test_input_missing_fields(#[case] input: Value, #[case] missing_field: &str) {
132+
let explorer = Explorer::new(String::from("mcp-example@mcp"));
133+
let url = explorer.create_explorer_url(&input);
134+
let filled_input = {
135+
let mut input = input;
136+
if missing_field == "document" {
137+
if let Some(obj) = input.as_object_mut() {
138+
obj.insert("document".to_string(), Value::String("{}".to_string()));
139+
}
140+
}
141+
if missing_field == "variables" {
142+
if let Some(obj) = input.as_object_mut() {
143+
obj.insert("variables".to_string(), Value::String("{}".to_string()));
144+
}
145+
}
146+
if missing_field == "headers" {
147+
if let Some(obj) = input.as_object_mut() {
148+
obj.insert("headers".to_string(), Value::String("{}".to_string()));
149+
}
150+
}
151+
input
152+
};
153+
let expected_url = explorer.create_explorer_url(&filled_input);
154+
assert_eq!(url, expected_url);
155+
}
139156
}

0 commit comments

Comments
 (0)