Skip to content

Commit f97e7af

Browse files
author
Matthew Hawkins
authored
Limit search results to just fields on root path (#195)
1 parent 3061508 commit f97e7af

File tree

15 files changed

+771
-277
lines changed

15 files changed

+771
-277
lines changed

clippy.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
allow-expect-in-tests = true
22
allow-panic-in-tests = true
33
allow-unwrap-in-tests = true
4+
allow-indexing-slicing-in-tests = true

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub struct Input {
3030
type_name: String,
3131
/// How far to recurse the type hierarchy. Use 0 for no limit. Defaults to 1.
3232
#[serde(default = "default_depth")]
33-
depth: u32,
33+
depth: usize,
3434
}
3535

3636
impl Introspect {
@@ -65,6 +65,7 @@ impl Introspect {
6565
match schema.types.get(type_name) {
6666
Some(extended_type) => tree_shaker.retain_type(
6767
extended_type,
68+
None,
6869
if input.depth > 0 {
6970
DepthLimit::Limited(input.depth)
7071
} else {
@@ -106,6 +107,6 @@ impl Introspect {
106107
}
107108

108109
/// The default depth to recurse the type hierarchy.
109-
fn default_depth() -> u32 {
110-
1u32
110+
fn default_depth() -> usize {
111+
1
111112
}

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

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
use crate::errors::McpError;
44
use crate::schema_from_type;
55
use crate::schema_tree_shake::{DepthLimit, SchemaTreeShaker};
6-
use apollo_compiler::Schema;
7-
use apollo_compiler::ast::OperationType as AstOperationType;
6+
use apollo_compiler::ast::{Field, OperationType as AstOperationType, Selection};
87
use apollo_compiler::validation::Valid;
8+
use apollo_compiler::{Name, Node, Schema};
99
use apollo_schema_index::{OperationType, Options, SchemaIndex};
1010
use rmcp::model::{CallToolResult, Content, ErrorCode, Tool};
1111
use rmcp::schemars::JsonSchema;
@@ -20,15 +20,16 @@ use tracing::debug;
2020
/// The name of the tool to search a GraphQL schema.
2121
pub const SEARCH_TOOL_NAME: &str = "search";
2222

23-
/// The depth of nested types to include for leaf nodes on matching root paths.
24-
pub const LEAF_DEPTH: DepthLimit = DepthLimit::Limited(1);
23+
/// The maximum number of search results to consider.
24+
const MAX_SEARCH_RESULTS: usize = 5;
2525

2626
/// A tool to search a GraphQL schema.
2727
#[derive(Clone)]
2828
pub struct Search {
2929
schema: Arc<Mutex<Valid<Schema>>>,
3030
index: SchemaIndex,
3131
allow_mutations: bool,
32+
leaf_depth: usize,
3233
pub tool: Tool,
3334
}
3435

@@ -53,6 +54,8 @@ impl Search {
5354
pub fn new(
5455
schema: Arc<Mutex<Valid<Schema>>>,
5556
allow_mutations: bool,
57+
leaf_depth: usize,
58+
index_memory_bytes: usize,
5659
) -> Result<Self, IndexingError> {
5760
let root_types = if allow_mutations {
5861
OperationType::Query | OperationType::Mutation
@@ -62,8 +65,9 @@ impl Search {
6265
let locked = &schema.try_lock()?;
6366
Ok(Self {
6467
schema: schema.clone(),
65-
index: SchemaIndex::new(locked, root_types)?,
68+
index: SchemaIndex::new(locked, root_types, index_memory_bytes)?,
6669
allow_mutations,
70+
leaf_depth,
6771
tool: Tool::new(
6872
SEARCH_TOOL_NAME,
6973
"Search a GraphQL schema",
@@ -84,7 +88,7 @@ impl Search {
8488
)
8589
})?;
8690

87-
root_paths.truncate(5);
91+
root_paths.truncate(MAX_SEARCH_RESULTS);
8892
debug!(
8993
"Root paths for search terms: {}\n{}",
9094
input.terms.join(", "),
@@ -98,16 +102,32 @@ impl Search {
98102
let schema = self.schema.lock().await;
99103
let mut tree_shaker = SchemaTreeShaker::new(&schema);
100104
for root_path in root_paths {
101-
let types = root_path.inner.types.clone();
102-
let path_len = types.len();
103-
for (i, type_name) in types.into_iter().enumerate() {
104-
if let Some(extended_type) = schema.types.get(type_name.as_ref()) {
105-
let depth = if i == path_len - 1 {
106-
LEAF_DEPTH
105+
let path_len = root_path.inner.len();
106+
for (i, path_node) in root_path.inner.into_iter().enumerate() {
107+
if let Some(extended_type) = schema.types.get(path_node.node_type.as_str()) {
108+
let (selection_set, depth) = if i == path_len - 1 {
109+
(None, DepthLimit::Limited(self.leaf_depth))
107110
} else {
108-
DepthLimit::Limited(1)
111+
(
112+
path_node.field_name.as_ref().map(|field_name| {
113+
vec![Selection::Field(Node::from(Field {
114+
alias: Default::default(),
115+
name: Name::new_unchecked(field_name),
116+
arguments: Default::default(),
117+
selection_set: Default::default(),
118+
directives: Default::default(),
119+
}))]
120+
}),
121+
DepthLimit::Limited(1),
122+
)
109123
};
110-
tree_shaker.retain_type(extended_type, depth)
124+
tree_shaker.retain_type(extended_type, selection_set.as_ref(), depth)
125+
}
126+
for field_arg in path_node.field_args {
127+
if let Some(extended_type) = schema.types.get(field_arg.as_str()) {
128+
// Retain input types with unlimited depth because all input must be given
129+
tree_shaker.retain_type(extended_type, None, DepthLimit::Unlimited);
130+
}
111131
}
112132
}
113133
}
@@ -171,7 +191,8 @@ mod tests {
171191
#[tokio::test]
172192
async fn test_search_tool(schema: Valid<Schema>) {
173193
let schema = Arc::new(Mutex::new(schema));
174-
let search = Search::new(schema.clone(), false).expect("Failed to create search tool");
194+
let search = Search::new(schema.clone(), false, 1, 15_000_000)
195+
.expect("Failed to create search tool");
175196

176197
let result = search
177198
.execute(Input {
@@ -188,7 +209,8 @@ mod tests {
188209
#[tokio::test]
189210
async fn test_referencing_types_are_collected(schema: Valid<Schema>) {
190211
let schema = Arc::new(Mutex::new(schema));
191-
let search = Search::new(schema.clone(), true).expect("Failed to create search tool");
212+
let search =
213+
Search::new(schema.clone(), true, 1, 15_000_000).expect("Failed to create search tool");
192214

193215
// Search for a type that should have references
194216
let result = search

crates/apollo-mcp-server/src/introspection/tools/snapshots/apollo_mcp_server__introspection__tools__search__tests__search_tool.snap

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,44 @@
22
source: crates/apollo-mcp-server/src/introspection/tools/search.rs
33
expression: content_to_snapshot(result)
44
---
5+
scalar DateTime
6+
57
enum UserRole {
68
ADMIN
79
MODERATOR
810
USER
911
GUEST
1012
}
1113

14+
enum ContentStatus {
15+
DRAFT
16+
PUBLISHED
17+
ARCHIVED
18+
DELETED
19+
}
20+
1221
type User implements Node {
13-
id: ID!
14-
createdAt: DateTime!
15-
updatedAt: DateTime!
16-
username: String!
17-
email: String!
1822
role: UserRole!
19-
profile: UserProfile
20-
posts: [Post!]!
21-
comments: [Comment!]!
22-
notifications: [Notification!]!
23-
preferences: UserPreferences!
2423
}
2524

2625
type Post implements Node & Content {
27-
id: ID!
28-
createdAt: DateTime!
29-
updatedAt: DateTime!
30-
title: String!
31-
content: String!
32-
status: ContentStatus!
3326
author: User!
34-
metadata: JSON
35-
comments: [Comment!]!
36-
media: [Media!]!
37-
tags: [Tag!]!
38-
analytics: PostAnalytics!
3927
}
4028

4129
type Query {
42-
node(id: ID!): Node
4330
user(id: ID!): User
4431
post(id: ID!): Post
4532
posts(filter: PostFilter): [Post!]!
46-
comments(postId: ID!): [Comment!]!
47-
notifications(filter: NotificationFilter): [Notification!]!
48-
search(query: String!): SearchResult!
4933
}
5034

51-
union SearchResult = User | Post
35+
input PostFilter {
36+
status: ContentStatus
37+
authorId: ID
38+
tags: [String!]
39+
dateRange: DateRangeInput
40+
}
41+
42+
input DateRangeInput {
43+
start: DateTime!
44+
end: DateTime!
45+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ async fn main() -> anyhow::Result<()> {
149149
.map(|custom_scalars_config| CustomScalarMap::try_from(&custom_scalars_config))
150150
.transpose()?,
151151
)
152+
.search_leaf_depth(config.introspection.search.leaf_depth)
153+
.index_memory_bytes(config.introspection.search.index_memory_bytes)
152154
.build()
153155
.start()
154156
.await?)

crates/apollo-mcp-server/src/runtime/introspection.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,29 @@ pub struct IntrospectConfig {
3232
}
3333

3434
/// Search tool configuration
35-
#[derive(Debug, Default, Deserialize, JsonSchema)]
35+
#[derive(Debug, Deserialize, JsonSchema)]
3636
#[serde(default)]
3737
pub struct SearchConfig {
3838
/// Enable search tool
3939
pub enabled: bool,
40+
41+
/// The amount of memory used for indexing (in bytes)
42+
pub index_memory_bytes: usize,
43+
44+
/// The depth of subtype information to include from matching types
45+
/// (1 is just the matching type, 2 is the matching type plus the types it references, etc.
46+
/// Defaults to 1.)
47+
pub leaf_depth: usize,
48+
}
49+
50+
impl Default for SearchConfig {
51+
fn default() -> Self {
52+
Self {
53+
enabled: false,
54+
index_memory_bytes: 50_000_000,
55+
leaf_depth: 1,
56+
}
57+
}
4058
}
4159

4260
impl Introspection {

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ impl RootOperationNames {
5252
#[derive(Debug, Clone, Copy)]
5353
pub enum DepthLimit {
5454
Unlimited,
55-
Limited(u32),
55+
Limited(usize),
5656
}
5757

5858
impl DepthLimit {
@@ -169,8 +169,13 @@ impl<'schema> SchemaTreeShaker<'schema> {
169169
}
170170

171171
/// Retain a specific type, and recursively every type it references, up to a given depth.
172-
pub fn retain_type(&mut self, retain: &ExtendedType, depth_limit: DepthLimit) {
173-
retain_type(self, retain, None, depth_limit);
172+
pub fn retain_type(
173+
&mut self,
174+
retain: &ExtendedType,
175+
selection_set: Option<&Vec<Selection>>,
176+
depth_limit: DepthLimit,
177+
) {
178+
retain_type(self, retain, selection_set, depth_limit);
174179
}
175180

176181
pub fn retain_operation(
@@ -1062,7 +1067,7 @@ mod test {
10621067
let query_type = nested_schema.types.get("Query").unwrap();
10631068

10641069
// Test with depth limit of 1
1065-
shaker.retain_type(query_type, DepthLimit::Limited(1));
1070+
shaker.retain_type(query_type, None, DepthLimit::Limited(1));
10661071
let shaken_schema = shaker.shaken().unwrap();
10671072

10681073
// Should retain only Query, not Level1, Level2, Level3, or Level4
@@ -1074,7 +1079,7 @@ mod test {
10741079

10751080
// Test with depth limit of 2
10761081
let mut shaker = SchemaTreeShaker::new(&nested_schema);
1077-
shaker.retain_type(query_type, DepthLimit::Limited(2));
1082+
shaker.retain_type(query_type, None, DepthLimit::Limited(2));
10781083
let shaken_schema = shaker.shaken().unwrap();
10791084

10801085
// Should retain Query and Level1, but not deeper levels
@@ -1087,7 +1092,7 @@ mod test {
10871092
// Test with depth limit of 1 starting from Level2
10881093
let mut shaker = SchemaTreeShaker::new(&nested_schema);
10891094
let level2_type = nested_schema.types.get("Level2").unwrap();
1090-
shaker.retain_type(level2_type, DepthLimit::Limited(1));
1095+
shaker.retain_type(level2_type, None, DepthLimit::Limited(1));
10911096
let shaken_schema = shaker.shaken().unwrap();
10921097

10931098
// Should retain only Level2
@@ -1098,7 +1103,7 @@ mod test {
10981103

10991104
// Test with depth limit of 2 starting from Level2
11001105
let mut shaker = SchemaTreeShaker::new(&nested_schema);
1101-
shaker.retain_type(level2_type, DepthLimit::Limited(2));
1106+
shaker.retain_type(level2_type, None, DepthLimit::Limited(2));
11021107
let shaken_schema = shaker.shaken().unwrap();
11031108

11041109
// Should retain Level2 and Level3
@@ -1109,7 +1114,7 @@ mod test {
11091114

11101115
// Test with depth limit of 5 starting from Level2
11111116
let mut shaker = SchemaTreeShaker::new(&nested_schema);
1112-
shaker.retain_type(level2_type, DepthLimit::Limited(5));
1117+
shaker.retain_type(level2_type, None, DepthLimit::Limited(5));
11131118
let shaken_schema = shaker.shaken().unwrap();
11141119

11151120
// Should retain Level2 and deeper types
@@ -1127,7 +1132,7 @@ mod test {
11271132
let query_type = nested_schema.types.get("Query").unwrap();
11281133

11291134
// Test with unlimited depth
1130-
shaker.retain_type(query_type, DepthLimit::Unlimited);
1135+
shaker.retain_type(query_type, None, DepthLimit::Unlimited);
11311136
let shaken_schema = shaker.shaken().unwrap();
11321137

11331138
// Should retain all types

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ pub struct Server {
3131
mutation_mode: MutationMode,
3232
disable_type_description: bool,
3333
disable_schema_description: bool,
34+
search_leaf_depth: usize,
35+
index_memory_bytes: usize,
3436
}
3537

3638
#[derive(Debug, Clone, Deserialize, Default, JsonSchema)]
@@ -93,6 +95,8 @@ impl Server {
9395
mutation_mode: MutationMode,
9496
disable_type_description: bool,
9597
disable_schema_description: bool,
98+
search_leaf_depth: usize,
99+
index_memory_bytes: usize,
96100
) -> Self {
97101
let headers = {
98102
let mut headers = headers.clone();
@@ -113,6 +117,8 @@ impl Server {
113117
mutation_mode,
114118
disable_type_description,
115119
disable_schema_description,
120+
search_leaf_depth,
121+
index_memory_bytes,
116122
}
117123
}
118124

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ struct Config {
4040
mutation_mode: MutationMode,
4141
disable_type_description: bool,
4242
disable_schema_description: bool,
43+
search_leaf_depth: usize,
44+
index_memory_bytes: usize,
4345
}
4446

4547
impl StateMachine {
@@ -66,6 +68,8 @@ impl StateMachine {
6668
mutation_mode: server.mutation_mode,
6769
disable_type_description: server.disable_type_description,
6870
disable_schema_description: server.disable_schema_description,
71+
search_leaf_depth: server.search_leaf_depth,
72+
index_memory_bytes: server.index_memory_bytes,
6973
},
7074
});
7175

crates/apollo-mcp-server/src/server/states/starting.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ impl Starting {
9494
Some(Search::new(
9595
schema.clone(),
9696
matches!(self.config.mutation_mode, MutationMode::All),
97+
self.config.search_leaf_depth,
98+
self.config.index_memory_bytes,
9799
)?)
98100
} else {
99101
None

0 commit comments

Comments
 (0)