Skip to content

Commit 3c10c1e

Browse files
author
Matthew Hawkins
authored
Minify introspect return value (#178)
1 parent af81241 commit 3c10c1e

File tree

10 files changed

+304
-22
lines changed

10 files changed

+304
-22
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Minify introspect return value - @pubmodmatt PR #178
2+
3+
The `introspect` and `search` tools now have an option to minify results. Minified GraphQL SDL takes up less space in the context window.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
//! Allow an AI agent to introspect a GraphQL schema.
22
3+
mod minify;
34
pub(crate) mod tools;
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
use apollo_compiler::schema::{ExtendedType, Type};
2+
use regex::Regex;
3+
use std::sync::OnceLock;
4+
5+
pub trait MinifyExt {
6+
/// Serialize in minified form
7+
fn minify(&self) -> String;
8+
}
9+
10+
impl MinifyExt for ExtendedType {
11+
fn minify(&self) -> String {
12+
match self {
13+
ExtendedType::Scalar(scalar_type) => minify_scalar(scalar_type),
14+
ExtendedType::Object(object_type) => minify_object(object_type),
15+
ExtendedType::Interface(interface_type) => minify_interface(interface_type),
16+
ExtendedType::Union(union_type) => minify_union(union_type),
17+
ExtendedType::Enum(enum_type) => minify_enum(enum_type),
18+
ExtendedType::InputObject(input_object_type) => minify_input_object(input_object_type),
19+
}
20+
}
21+
}
22+
23+
fn minify_scalar(scalar_type: &apollo_compiler::schema::ScalarType) -> String {
24+
shorten_scalar_names(scalar_type.name.as_str()).to_string()
25+
}
26+
27+
fn minify_object(object_type: &apollo_compiler::schema::ObjectType) -> String {
28+
let fields = minify_fields(&object_type.fields);
29+
let type_name = format_type_name_with_description(&object_type.name, &object_type.description);
30+
let interfaces = format_interfaces(&object_type.implements_interfaces);
31+
32+
if interfaces.is_empty() {
33+
format!("T:{type_name}:{fields}")
34+
} else {
35+
format!("T:{type_name}<{interfaces}>:{fields}")
36+
}
37+
}
38+
39+
fn minify_interface(interface_type: &apollo_compiler::schema::InterfaceType) -> String {
40+
let fields = minify_fields(&interface_type.fields);
41+
let type_name =
42+
format_type_name_with_description(&interface_type.name, &interface_type.description);
43+
format!("F:{type_name}:{fields}")
44+
}
45+
46+
fn minify_union(union_type: &apollo_compiler::schema::UnionType) -> String {
47+
let member_types = union_type
48+
.members
49+
.iter()
50+
.map(|member| member.as_str())
51+
.collect::<Vec<&str>>()
52+
.join(",");
53+
let type_name = format_type_name_with_description(&union_type.name, &union_type.description);
54+
format!("U:{type_name}:{member_types}")
55+
}
56+
57+
fn minify_enum(enum_type: &apollo_compiler::schema::EnumType) -> String {
58+
let values = enum_type
59+
.values
60+
.keys()
61+
.map(|value| value.as_str())
62+
.collect::<Vec<&str>>()
63+
.join(",");
64+
let type_name = format_type_name_with_description(&enum_type.name, &enum_type.description);
65+
format!("E:{type_name}:{values}")
66+
}
67+
68+
fn minify_input_object(input_object_type: &apollo_compiler::schema::InputObjectType) -> String {
69+
let fields = minify_input_fields(&input_object_type.fields);
70+
let type_name =
71+
format_type_name_with_description(&input_object_type.name, &input_object_type.description);
72+
format!("I:{type_name}:{fields}")
73+
}
74+
75+
fn minify_fields(
76+
fields: &apollo_compiler::collections::IndexMap<
77+
apollo_compiler::Name,
78+
apollo_compiler::schema::Component<apollo_compiler::ast::FieldDefinition>,
79+
>,
80+
) -> String {
81+
let mut result = String::new();
82+
83+
for (field_name, field) in fields.iter() {
84+
// Add description if present
85+
if let Some(desc) = field.description.as_ref() {
86+
result.push_str(&format!("\"{}\"", normalize_description(desc)));
87+
}
88+
89+
// Add field name
90+
result.push_str(field_name.as_str());
91+
92+
// Add arguments if present
93+
if !field.arguments.is_empty() {
94+
result.push('(');
95+
result.push_str(&minify_arguments(&field.arguments));
96+
result.push(')');
97+
}
98+
99+
// Add field type
100+
result.push(':');
101+
result.push_str(&type_name(&field.ty));
102+
result.push(',');
103+
}
104+
105+
// Remove trailing comma
106+
if !result.is_empty() {
107+
result.pop();
108+
}
109+
110+
result
111+
}
112+
113+
fn minify_input_fields(
114+
fields: &apollo_compiler::collections::IndexMap<
115+
apollo_compiler::Name,
116+
apollo_compiler::schema::Component<apollo_compiler::ast::InputValueDefinition>,
117+
>,
118+
) -> String {
119+
let mut result = String::new();
120+
121+
for (field_name, field) in fields.iter() {
122+
// Add description if present
123+
if let Some(desc) = field.description.as_ref() {
124+
result.push_str(&format!("\"{}\"", normalize_description(desc)));
125+
}
126+
127+
// Add field name and type
128+
result.push_str(field_name.as_str());
129+
result.push(':');
130+
result.push_str(&type_name(&field.ty));
131+
result.push(',');
132+
}
133+
134+
// Remove trailing comma
135+
if !result.is_empty() {
136+
result.pop();
137+
}
138+
139+
result
140+
}
141+
142+
fn minify_arguments(
143+
arguments: &[apollo_compiler::Node<apollo_compiler::ast::InputValueDefinition>],
144+
) -> String {
145+
arguments
146+
.iter()
147+
.map(|arg| {
148+
if let Some(desc) = arg.description.as_ref() {
149+
format!(
150+
"\"{}\"{}:{}",
151+
normalize_description(desc),
152+
arg.name.as_str(),
153+
type_name(&arg.ty)
154+
)
155+
} else {
156+
format!("{}:{}", arg.name.as_str(), type_name(&arg.ty))
157+
}
158+
})
159+
.collect::<Vec<String>>()
160+
.join(",")
161+
}
162+
163+
fn format_type_name_with_description(
164+
name: &apollo_compiler::Name,
165+
description: &Option<apollo_compiler::Node<str>>,
166+
) -> String {
167+
if let Some(desc) = description.as_ref() {
168+
format!("\"{}\"{}", normalize_description(desc), name)
169+
} else {
170+
name.to_string()
171+
}
172+
}
173+
174+
fn format_interfaces(
175+
interfaces: &apollo_compiler::collections::IndexSet<apollo_compiler::schema::ComponentName>,
176+
) -> String {
177+
interfaces
178+
.iter()
179+
.map(|interface| interface.as_str())
180+
.collect::<Vec<&str>>()
181+
.join(",")
182+
}
183+
184+
fn type_name(ty: &Type) -> String {
185+
let name = shorten_scalar_names(ty.inner_named_type().as_str());
186+
if ty.is_list() {
187+
format!("[{name}]")
188+
} else if ty.is_non_null() {
189+
format!("{name}!")
190+
} else {
191+
name.to_string()
192+
}
193+
}
194+
195+
fn shorten_scalar_names(name: &str) -> &str {
196+
match name {
197+
"String" => "s",
198+
"Int" => "i",
199+
"Float" => "f",
200+
"Boolean" => "b",
201+
"ID" => "d",
202+
_ => name,
203+
}
204+
}
205+
206+
/// Normalize description formatting
207+
#[allow(clippy::expect_used)]
208+
fn normalize_description(desc: &str) -> String {
209+
// LLMs can typically process descriptions just fine without whitespace
210+
static WHITESPACE_PATTERN: OnceLock<Regex> = OnceLock::new();
211+
let re = WHITESPACE_PATTERN.get_or_init(|| Regex::new(r"\s+").expect("regex pattern compiles"));
212+
re.replace_all(desc, "").to_string()
213+
}

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

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::errors::McpError;
2+
use crate::introspection::minify::MinifyExt as _;
23
use crate::schema_from_type;
34
use crate::schema_tree_shake::{DepthLimit, SchemaTreeShaker};
45
use apollo_compiler::Schema;
56
use apollo_compiler::ast::OperationType;
7+
use apollo_compiler::schema::ExtendedType;
68
use apollo_compiler::validation::Valid;
79
use rmcp::model::{CallToolResult, Content, Tool};
810
use rmcp::schemars::JsonSchema;
@@ -20,6 +22,7 @@ pub const INTROSPECT_TOOL_NAME: &str = "introspect";
2022
pub struct Introspect {
2123
schema: Arc<Mutex<Valid<Schema>>>,
2224
allow_mutations: bool,
25+
minify: bool,
2326
pub tool: Tool,
2427
}
2528

@@ -38,21 +41,15 @@ impl Introspect {
3841
schema: Arc<Mutex<Valid<Schema>>>,
3942
root_query_type: Option<String>,
4043
root_mutation_type: Option<String>,
44+
minify: bool,
4145
) -> Self {
4246
Self {
4347
schema,
4448
allow_mutations: root_mutation_type.is_some(),
49+
minify,
4550
tool: Tool::new(
4651
INTROSPECT_TOOL_NAME,
47-
format!(
48-
"Get detailed information about types from the GraphQL schema.{}{}",
49-
root_query_type
50-
.map(|t| format!(" Use the type name `{t}` to get root query fields."))
51-
.unwrap_or_default(),
52-
root_mutation_type
53-
.map(|t| format!(" Use the type name `{t}` to get root mutation fields."))
54-
.unwrap_or_default()
55-
),
52+
tool_description(root_query_type, root_mutation_type, minify),
5653
schema_from_type!(Input),
5754
),
5855
}
@@ -97,13 +94,41 @@ impl Introspect {
9794
.root_operation(OperationType::Subscription)
9895
.is_none_or(|root_name| extended_type.name() != root_name)
9996
})
100-
.map(|(_, extended_type)| extended_type.serialize())
101-
.map(|serialized| serialized.to_string())
97+
.map(|(_, extended_type)| extended_type)
98+
.map(|extended_type| self.serialize(extended_type))
10299
.map(Content::text)
103100
.collect(),
104101
is_error: None,
105102
})
106103
}
104+
105+
fn serialize(&self, extended_type: &ExtendedType) -> String {
106+
if self.minify {
107+
extended_type.minify()
108+
} else {
109+
extended_type.serialize().to_string()
110+
}
111+
}
112+
}
113+
114+
fn tool_description(
115+
root_query_type: Option<String>,
116+
root_mutation_type: Option<String>,
117+
minify: bool,
118+
) -> String {
119+
if minify {
120+
"Get GraphQL type information - T=type,I=input,E=enum,U=union,F=interface;s=String,i=Int,f=Float,b=Boolean,d=ID;!=required,[]=list,<>=implements;".to_string()
121+
} else {
122+
format!(
123+
"Get detailed information about types from the GraphQL schema.{}{}",
124+
root_query_type
125+
.map(|t| format!(" Use the type name `{t}` to get root query fields."))
126+
.unwrap_or_default(),
127+
root_mutation_type
128+
.map(|t| format!(" Use the type name `{t}` to get root mutation fields."))
129+
.unwrap_or_default()
130+
)
131+
}
107132
}
108133

109134
/// The default depth to recurse the type hierarchy.

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! MCP tool to search a GraphQL schema.
22
33
use crate::errors::McpError;
4+
use crate::introspection::minify::MinifyExt as _;
45
use crate::schema_from_type;
56
use crate::schema_tree_shake::{DepthLimit, SchemaTreeShaker};
67
use apollo_compiler::ast::{Field, OperationType as AstOperationType, Selection};
@@ -30,6 +31,7 @@ pub struct Search {
3031
index: SchemaIndex,
3132
allow_mutations: bool,
3233
leaf_depth: usize,
34+
minify: bool,
3335
pub tool: Tool,
3436
}
3537

@@ -56,6 +58,7 @@ impl Search {
5658
allow_mutations: bool,
5759
leaf_depth: usize,
5860
index_memory_bytes: usize,
61+
minify: bool,
5962
) -> Result<Self, IndexingError> {
6063
let root_types = if allow_mutations {
6164
OperationType::Query | OperationType::Mutation
@@ -68,9 +71,17 @@ impl Search {
6871
index: SchemaIndex::new(locked, root_types, index_memory_bytes)?,
6972
allow_mutations,
7073
leaf_depth,
74+
minify,
7175
tool: Tool::new(
7276
SEARCH_TOOL_NAME,
73-
"Search a GraphQL schema",
77+
format!(
78+
"Search a GraphQL schema{}",
79+
if minify {
80+
" - T=type,I=input,E=enum,U=union,F=interface;s=String,i=Int,f=Float,b=Boolean,d=ID;!=required,[]=list,<>=implements"
81+
} else {
82+
""
83+
}
84+
),
7485
schema_from_type!(Input),
7586
),
7687
})
@@ -146,8 +157,13 @@ impl Search {
146157
extended_type.name() != root_name || self.allow_mutations
147158
})
148159
})
149-
.map(|(_, extended_type)| extended_type.serialize())
150-
.map(|serialized| serialized.to_string())
160+
.map(|(_, extended_type)| {
161+
if self.minify {
162+
extended_type.minify()
163+
} else {
164+
extended_type.serialize().to_string()
165+
}
166+
})
151167
.map(Content::text)
152168
.collect(),
153169
is_error: None,
@@ -191,7 +207,7 @@ mod tests {
191207
#[tokio::test]
192208
async fn test_search_tool(schema: Valid<Schema>) {
193209
let schema = Arc::new(Mutex::new(schema));
194-
let search = Search::new(schema.clone(), false, 1, 15_000_000)
210+
let search = Search::new(schema.clone(), false, 1, 15_000_000, false)
195211
.expect("Failed to create search tool");
196212

197213
let result = search
@@ -209,8 +225,8 @@ mod tests {
209225
#[tokio::test]
210226
async fn test_referencing_types_are_collected(schema: Valid<Schema>) {
211227
let schema = Arc::new(Mutex::new(schema));
212-
let search =
213-
Search::new(schema.clone(), true, 1, 15_000_000).expect("Failed to create search tool");
228+
let search = Search::new(schema.clone(), true, 1, 15_000_000, false)
229+
.expect("Failed to create search tool");
214230

215231
// Search for a type that should have references
216232
let result = search

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ async fn main() -> anyhow::Result<()> {
139139
.headers(config.headers)
140140
.execute_introspection(config.introspection.execute.enabled)
141141
.introspect_introspection(config.introspection.introspect.enabled)
142+
.introspect_minify(config.introspection.introspect.minify)
143+
.search_minify(config.introspection.search.minify)
142144
.search_introspection(config.introspection.search.enabled)
143145
.mutation_mode(config.overrides.mutation_mode)
144146
.disable_type_description(config.overrides.disable_type_description)

0 commit comments

Comments
 (0)