Skip to content

Commit 0a260ce

Browse files
committed
Fix tool to open Apollo Explorer
1 parent 1674cdd commit 0a260ce

File tree

4 files changed

+99
-13
lines changed

4 files changed

+99
-13
lines changed

Cargo.lock

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ package.version = "0.1.0"
66

77
[workspace.dependencies]
88
apollo-compiler = "1.27.0"
9+
insta = { version = "1.43.1", features = [
10+
"json",
11+
"redactions",
12+
"yaml",
13+
"glob",
14+
] }
915
serde = { version = "1.0.219", features = ["derive"] }
1016
serde_json = "1.0.140"
1117
thiserror = "2.0.12"

crates/mcp-apollo-server/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@ tracing.workspace = true
3434
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
3535
regex = "1.11.1"
3636
webbrowser = "1.0.4"
37-
base64 = "0.22.1"
3837

3938
[dev-dependencies]
40-
insta = "1.42.2"
39+
insta = { workspace = true }
4140

4241
[lints]
4342
workspace = true

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

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::errors::McpError;
22
use crate::schema_from_type;
3-
use base64::Engine;
43
use rmcp::model::{CallToolResult, Content, ErrorCode, Tool};
54
use rmcp::schemars::JsonSchema;
65
use rmcp::serde_json::Value;
76
use rmcp::{schemars, serde_json};
87
use serde::Deserialize;
8+
use tracing::info;
99

1010
pub(crate) const EXPLORER_TOOL_NAME: &str = "explorer";
1111

@@ -36,24 +36,77 @@ impl Explorer {
3636
variant,
3737
tool: Tool::new(
3838
EXPLORER_TOOL_NAME,
39-
"Open a GraphQL operation in Apollo Explorer",
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 `{}`",
4040
schema_from_type!(Input),
4141
),
4242
}
4343
}
4444

4545
fn create_explorer_url(&self, input: &Value) -> String {
46-
let compressed = lz_str::compress_to_uint8_array(input.to_string().as_str());
47-
let encoded = base64::engine::general_purpose::STANDARD.encode(compressed);
46+
let compressed = lz_str::compress_to_encoded_uri_component(input.to_string().as_str());
4847
format!(
49-
"https://studio.apollographql.com/graph/{graph_id}/variant/{variant}/explorer?explorerURLState={encoded}",
48+
"https://studio.apollographql.com/graph/{graph_id}/variant/{variant}/explorer?explorerURLState={compressed}",
5049
graph_id = self.graph_id,
5150
variant = self.variant
5251
)
5352
}
5453

5554
pub async fn execute(&self, input: Value) -> Result<CallToolResult, McpError> {
56-
webbrowser::open(self.create_explorer_url(&input).as_str())
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+
}
76+
77+
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+
81+
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+
));
87+
}
88+
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+
));
94+
}
95+
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+
));
101+
}
102+
103+
let url = self.create_explorer_url(&input);
104+
info!(
105+
"Opening Apollo Explorer URL: {} for input operation: {}",
106+
url,
107+
serde_json::to_string_pretty(&input).unwrap_or("<unable to serialize>".into())
108+
);
109+
webbrowser::open(url.as_str())
57110
.map(|_| CallToolResult {
58111
content: vec![Content::text("success")],
59112
is_error: None,
@@ -65,21 +118,22 @@ impl Explorer {
65118
#[cfg(test)]
66119
mod tests {
67120
use super::*;
121+
use insta::assert_snapshot;
68122
use rmcp::serde_json::json;
69123

70124
#[test]
71125
fn test_create_explorer_url() {
72126
let explorer = Explorer::new(String::from("mcp-example@mcp"));
73127
let input = json!({
74128
"document": "query GetWeatherAlerts($state: String!) {\n alerts(state: $state) {\n severity\n description\n instruction\n }\n}",
75-
"headers": "{}",
76-
"variables": "{\"state\": \"CA\"}"
129+
"variables": "{\"state\": \"CA\"}",
130+
"headers": "{}"
77131
});
78132

79133
let url = explorer.create_explorer_url(&input);
80-
assert_eq!(
134+
assert_snapshot!(
81135
url,
82-
"https://studio.apollographql.com/graph/mcp-example/variant/mcp/explorer?explorerURLState=N4IgJg9gxgrgtgUwHYBcQC4QEcYIE4CeABAOIIoDqCAhigBb4CCANvigM4AUAJOyrQnREAyijwBLJAHMAhAEoiwADpIiRaqzwdOfAUN78UCBctVqi7BADd84lARXmiYBOygSADinEQkj85J8eDBQ3r7+AL4qESAANCAM1C547BggwDHxVtQS1ABGrKmYyiC6RkoYRBUAwowVMRFAAAA="
136+
@"https://studio.apollographql.com/graph/mcp-example/variant/mcp/explorer?explorerURLState=N4IgJg9gxgrgtgUwHYBcQC4QEcYIE4CeABAOIIoDqCAhigBb4CCANvigM4AUAJOyrQnREAyijwBLJAHMAhAEoiwADpIiRaqzwdOfAUN78UCBctVqi7BADd84lARXmiYBOygSADinEQkj85J8eDBQ3r7+AL4qESAANCAM1C547BggwDHxVtQS1ABGrKmYyiC6RkoYRBUAwowVMRFAA"
83137
);
84138
}
85139
}

0 commit comments

Comments
 (0)