1
1
use crate :: errors:: McpError ;
2
2
use crate :: schema_from_type;
3
- use base64:: Engine ;
4
3
use rmcp:: model:: { CallToolResult , Content , ErrorCode , Tool } ;
5
4
use rmcp:: schemars:: JsonSchema ;
6
5
use rmcp:: serde_json:: Value ;
7
6
use rmcp:: { schemars, serde_json} ;
8
7
use serde:: Deserialize ;
8
+ use tracing:: info;
9
9
10
10
pub ( crate ) const EXPLORER_TOOL_NAME : & str = "explorer" ;
11
11
@@ -36,24 +36,77 @@ impl Explorer {
36
36
variant,
37
37
tool : Tool :: new (
38
38
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 `{}` " ,
40
40
schema_from_type ! ( Input ) ,
41
41
) ,
42
42
}
43
43
}
44
44
45
45
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 ( ) ) ;
48
47
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 }" ,
50
49
graph_id = self . graph_id,
51
50
variant = self . variant
52
51
)
53
52
}
54
53
55
54
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 ( ) )
57
110
. map ( |_| CallToolResult {
58
111
content : vec ! [ Content :: text( "success" ) ] ,
59
112
is_error : None ,
@@ -65,21 +118,22 @@ impl Explorer {
65
118
#[ cfg( test) ]
66
119
mod tests {
67
120
use super :: * ;
121
+ use insta:: assert_snapshot;
68
122
use rmcp:: serde_json:: json;
69
123
70
124
#[ test]
71
125
fn test_create_explorer_url ( ) {
72
126
let explorer = Explorer :: new ( String :: from ( "mcp-example@mcp" ) ) ;
73
127
let input = json ! ( {
74
128
"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 " : "{}"
77
131
} ) ;
78
132
79
133
let url = explorer. create_explorer_url ( & input) ;
80
- assert_eq ! (
134
+ assert_snapshot ! (
81
135
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 "
83
137
) ;
84
138
}
85
139
}
0 commit comments