Skip to content

Commit bc7abdd

Browse files
authored
Merge pull request #447 from apollographql/AIR-49
Fix compatibility issue with VSCode/Copilot
2 parents 4b90ce4 + f137908 commit bc7abdd

File tree

4 files changed

+172
-3
lines changed

4 files changed

+172
-3
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Fix compatibility issue with VSCode/Copilot - @DaleSeo PR #447
2+
3+
This updates Apollo MCP Server’s tool schemas from [Draft 2020-12](https://json-schema.org/draft/2020-12) to [Draft‑07](https://json-schema.org/draft-07) which is more widely supported across different validators. VSCode/Copilot still validate against Draft‑07, so rejects Apollo MCP Server’s tools. Our JSON schemas don’t rely on newer features, so downgrading improves compatibility across MCP clients with no practical impact.

crates/apollo-mcp-server/src/introspection/tools/introspect.rs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,10 @@ impl Introspect {
9090
&& schema
9191
.root_operation(OperationType::Mutation)
9292
.is_none_or(|root_name| {
93+
// Allow introspection of the mutation type itself even when mutations are disabled
9394
extended_type.name() != root_name
94-
|| (type_name == root_name.as_str() && self.allow_mutations)
95+
|| type_name == root_name.as_str()
96+
|| self.allow_mutations
9597
})
9698
&& schema
9799
.root_operation(OperationType::Subscription)
@@ -188,4 +190,131 @@ mod tests {
188190
assert!(description.contains("T=type,I=input,E=enum,U=union,F=interface"));
189191
assert!(description.contains("s=String,i=Int,f=Float,b=Boolean,d=ID"));
190192
}
193+
194+
#[rstest]
195+
#[tokio::test]
196+
async fn test_introspect_query_depth_1_returns_fields(schema: Valid<Schema>) {
197+
let introspect = Introspect::new(
198+
Arc::new(Mutex::new(schema)),
199+
Some("Query".to_string()),
200+
Some("Mutation".to_string()),
201+
false,
202+
);
203+
204+
let result = introspect
205+
.execute(Input {
206+
type_name: "Query".to_string(),
207+
depth: 1,
208+
})
209+
.await
210+
.expect("Introspect execution failed");
211+
212+
let content = result
213+
.content
214+
.iter()
215+
.filter_map(|c| {
216+
use rmcp::model::RawContent;
217+
use std::ops::Deref;
218+
let c = c.deref();
219+
match c {
220+
RawContent::Text(text) => Some(text.text.clone()),
221+
_ => None,
222+
}
223+
})
224+
.collect::<Vec<String>>()
225+
.join("\n");
226+
227+
// Query with depth 1 should return the Query type with its fields
228+
assert!(!result.content.is_empty());
229+
assert!(content.contains("type Query"));
230+
}
231+
232+
#[rstest]
233+
#[tokio::test]
234+
async fn test_introspect_mutation_depth_1_returns_fields(schema: Valid<Schema>) {
235+
let introspect = Introspect::new(
236+
Arc::new(Mutex::new(schema)),
237+
Some("Query".to_string()),
238+
Some("Mutation".to_string()),
239+
false,
240+
);
241+
242+
let result = introspect
243+
.execute(Input {
244+
type_name: "Mutation".to_string(),
245+
depth: 1,
246+
})
247+
.await
248+
.expect("Introspect execution failed");
249+
250+
let content = result
251+
.content
252+
.iter()
253+
.filter_map(|c| {
254+
use rmcp::model::RawContent;
255+
use std::ops::Deref;
256+
let c = c.deref();
257+
match c {
258+
RawContent::Text(text) => Some(text.text.clone()),
259+
_ => None,
260+
}
261+
})
262+
.collect::<Vec<String>>()
263+
.join("\n");
264+
265+
// Mutation with depth 1 should return the Mutation type with its fields, just like Query
266+
assert!(
267+
!result.content.is_empty(),
268+
"Mutation introspection should return content"
269+
);
270+
assert!(
271+
content.contains("type Mutation"),
272+
"Should contain Mutation type definition"
273+
);
274+
}
275+
276+
#[rstest]
277+
#[tokio::test]
278+
async fn test_introspect_mutation_depth_1_with_mutations_disabled(schema: Valid<Schema>) {
279+
// This test verifies the fix: when mutations are not allowed, mutation introspection should still work
280+
let introspect = Introspect::new(
281+
Arc::new(Mutex::new(schema)),
282+
Some("Query".to_string()),
283+
None,
284+
false,
285+
);
286+
287+
let result = introspect
288+
.execute(Input {
289+
type_name: "Mutation".to_string(),
290+
depth: 1,
291+
})
292+
.await
293+
.expect("Introspect execution failed");
294+
295+
let content = result
296+
.content
297+
.iter()
298+
.filter_map(|c| {
299+
use rmcp::model::RawContent;
300+
use std::ops::Deref;
301+
let c = c.deref();
302+
match c {
303+
RawContent::Text(text) => Some(text.text.clone()),
304+
_ => None,
305+
}
306+
})
307+
.collect::<Vec<String>>()
308+
.join("\n");
309+
310+
// After the fix: mutation introspection should work even when mutations are disabled
311+
assert!(
312+
!result.content.is_empty(),
313+
"Mutation introspection should return content even when mutations are disabled"
314+
);
315+
assert!(
316+
content.contains("type Mutation"),
317+
"Should contain Mutation type definition"
318+
);
319+
}
191320
}

crates/apollo-mcp-server/src/introspection/tools/validate.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use rmcp::schemars::JsonSchema;
1111
use rmcp::serde_json::Value;
1212
use rmcp::{schemars, serde_json};
1313
use serde::Deserialize;
14-
use std::default::Default;
1514
use std::sync::Arc;
1615
use tokio::sync::Mutex;
1716

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,47 @@
22
#[macro_export]
33
macro_rules! schema_from_type {
44
($type:ty) => {{
5-
match serde_json::to_value(schemars::schema_for!($type)) {
5+
// Use Draft-07 for compatibility with MCP clients like VSCode/Copilot that don't support newer drafts.
6+
// See: https://github.com/microsoft/vscode/issues/251315
7+
let settings = schemars::generate::SchemaSettings::draft07();
8+
let generator = settings.into_generator();
9+
let schema = generator.into_root_schema_for::<$type>();
10+
match serde_json::to_value(schema) {
611
Ok(Value::Object(schema)) => schema,
712
_ => panic!("Failed to generate schema for {}", stringify!($type)),
813
}
914
}};
1015
}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use schemars::JsonSchema;
20+
use serde::Deserialize;
21+
use serde_json::Value;
22+
23+
#[derive(JsonSchema, Deserialize)]
24+
struct TestInput {
25+
#[allow(dead_code)]
26+
field: String,
27+
}
28+
29+
#[test]
30+
fn test_schema_from_type() {
31+
let schema = schema_from_type!(TestInput);
32+
33+
assert_eq!(
34+
serde_json::to_value(&schema).unwrap(),
35+
serde_json::json!({
36+
"$schema": "http://json-schema.org/draft-07/schema#",
37+
"title": "TestInput",
38+
"type": "object",
39+
"properties": {
40+
"field": {
41+
"type": "string"
42+
}
43+
},
44+
"required": ["field"]
45+
})
46+
);
47+
}
48+
}

0 commit comments

Comments
 (0)