Skip to content

Commit f137908

Browse files
committed
chore: add changeset
1 parent 85ce67d commit f137908

File tree

2 files changed

+133
-1
lines changed

2 files changed

+133
-1
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
}

0 commit comments

Comments
 (0)