Skip to content

Commit 131a9e0

Browse files
authored
Merge pull request #367 from apollographql/evan/minify/include-builtin-directives
Minify: Add support for deprecated directive
2 parents a300de5 + 8d83382 commit 131a9e0

File tree

8 files changed

+142
-7
lines changed

8 files changed

+142
-7
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Minify: Add support for deprecated directive - @esilverm PR #367
2+
3+
Includes any existing `@deprecated` directives in the schema in the minified output of builtin tools. Now operations generated via these tools should take into account deprecated fields when being generated.

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

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use apollo_compiler::schema::{ExtendedType, Type};
22
use regex::Regex;
3-
use std::sync::OnceLock;
3+
use std::{collections::HashMap, sync::OnceLock};
44

55
pub trait MinifyExt {
66
/// Serialize in minified form
@@ -72,6 +72,38 @@ fn minify_input_object(input_object_type: &apollo_compiler::schema::InputObjectT
7272
format!("I:{type_name}:{fields}")
7373
}
7474

75+
// We should only minify directives that assist the LLM in understanding the schema. This included @deprecated
76+
fn minify_directives(directives: &apollo_compiler::ast::DirectiveList) -> String {
77+
let mut result = String::new();
78+
79+
static DIRECTIVES_TO_MINIFY: OnceLock<HashMap<&str, &str>> = OnceLock::new();
80+
let directives_to_minify =
81+
DIRECTIVES_TO_MINIFY.get_or_init(|| HashMap::from([("deprecated", "D")]));
82+
83+
for directive in directives.iter() {
84+
if let Some(minified_name) = directives_to_minify.get(directive.name.as_str()) {
85+
// Since we're only handling @deprecated right now we can just add the reason and minify it.
86+
// We should handle this more generically in the future.
87+
if !directive.arguments.is_empty()
88+
&& let Some(reason) = directive
89+
.arguments
90+
.iter()
91+
.find(|a| a.name == "reason")
92+
.and_then(|a| a.value.as_str())
93+
{
94+
result.push_str(&format!(
95+
"@{}(\"{}\")",
96+
minified_name,
97+
normalize_description(reason)
98+
));
99+
} else {
100+
result.push_str(&format!("@{}", minified_name));
101+
}
102+
}
103+
}
104+
result
105+
}
106+
75107
fn minify_fields(
76108
fields: &apollo_compiler::collections::IndexMap<
77109
apollo_compiler::Name,
@@ -99,6 +131,8 @@ fn minify_fields(
99131
// Add field type
100132
result.push(':');
101133
result.push_str(&type_name(&field.ty));
134+
result.push_str(&minify_directives(&field.directives));
135+
102136
result.push(',');
103137
}
104138

@@ -128,6 +162,7 @@ fn minify_input_fields(
128162
result.push_str(field_name.as_str());
129163
result.push(':');
130164
result.push_str(&type_name(&field.ty));
165+
result.push_str(&minify_directives(&field.directives));
131166
result.push(',');
132167
}
133168

@@ -147,13 +182,19 @@ fn minify_arguments(
147182
.map(|arg| {
148183
if let Some(desc) = arg.description.as_ref() {
149184
format!(
150-
"\"{}\"{}:{}",
185+
"\"{}\"{}:{}{}",
151186
normalize_description(desc),
152187
arg.name.as_str(),
153-
type_name(&arg.ty)
188+
type_name(&arg.ty),
189+
minify_directives(&arg.directives)
154190
)
155191
} else {
156-
format!("{}:{}", arg.name.as_str(), type_name(&arg.ty))
192+
format!(
193+
"{}:{}{}",
194+
arg.name.as_str(),
195+
type_name(&arg.ty),
196+
minify_directives(&arg.directives)
197+
)
157198
}
158199
})
159200
.collect::<Vec<String>>()
@@ -211,3 +252,27 @@ fn normalize_description(desc: &str) -> String {
211252
let re = WHITESPACE_PATTERN.get_or_init(|| Regex::new(r"\s+").expect("regex pattern compiles"));
212253
re.replace_all(desc, "").to_string()
213254
}
255+
256+
#[cfg(test)]
257+
mod tests {
258+
use super::*;
259+
260+
const TEST_SCHEMA: &str = include_str!("tools/testdata/schema.graphql");
261+
262+
#[test]
263+
fn test_minify_schema() {
264+
let schema = apollo_compiler::schema::Schema::parse(TEST_SCHEMA, "schema.graphql")
265+
.expect("Failed to parse schema")
266+
.validate()
267+
.expect("Failed to validate schema");
268+
269+
let minified = schema
270+
.types
271+
.iter()
272+
.map(|(_, type_)| format!("{}: {}", type_.name().as_str(), type_.minify()))
273+
.collect::<Vec<String>>()
274+
.join("\n");
275+
276+
insta::assert_snapshot!(minified);
277+
}
278+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
source: crates/apollo-mcp-server/src/introspection/minify.rs
3+
expression: minified
4+
---
5+
__Schema: T:"AGraphQLSchemadefinesthecapabilitiesofaGraphQLserver.Itexposesallavailabletypesanddirectivesontheserver,aswellastheentrypointsforquery,mutation,andsubscriptionoperations."__Schema:description:s,"Alistofalltypessupportedbythisserver."types:[__Type],"Thetypethatqueryoperationswillberootedat."queryType:__Type!,"Ifthisserversupportsmutation,thetypethatmutationoperationswillberootedat."mutationType:__Type,"Ifthisserversupportsubscription,thetypethatsubscriptionoperationswillberootedat."subscriptionType:__Type,"Alistofalldirectivessupportedbythisserver."directives:[__Directive]
6+
__Type: T:"ThefundamentalunitofanyGraphQLSchemaisthetype.TherearemanykindsoftypesinGraphQLasrepresentedbythe`__TypeKind`enum.Dependingonthekindofatype,certainfieldsdescribeinformationaboutthattype.Scalartypesprovidenoinformationbeyondaname,descriptionandoptional`specifiedByURL`,whileEnumtypesprovidetheirvalues.ObjectandInterfacetypesprovidethefieldstheydescribe.Abstracttypes,UnionandInterface,providetheObjecttypespossibleatruntime.ListandNonNulltypescomposeothertypes."__Type:kind:__TypeKind!,name:s,description:s,fields(includeDeprecated:b):[__Field],interfaces:[__Type],possibleTypes:[__Type],enumValues(includeDeprecated:b):[__EnumValue],inputFields(includeDeprecated:b):[__InputValue],ofType:__Type,specifiedByURL:s
7+
__TypeKind: E:"Anenumdescribingwhatkindoftypeagiven`__Type`is."__TypeKind:SCALAR,OBJECT,INTERFACE,UNION,ENUM,INPUT_OBJECT,LIST,NON_NULL
8+
__Field: T:"ObjectandInterfacetypesaredescribedbyalistofFields,eachofwhichhasaname,potentiallyalistofarguments,andareturntype."__Field:name:s!,description:s,args(includeDeprecated:b):[__InputValue],type:__Type!,isDeprecated:b!,deprecationReason:s
9+
__InputValue: T:"ArgumentsprovidedtoFieldsorDirectivesandtheinputfieldsofanInputObjectarerepresentedasInputValueswhichdescribetheirtypeandoptionallyadefaultvalue."__InputValue:name:s!,description:s,type:__Type!,"AGraphQL-formattedstringrepresentingthedefaultvalueforthisinputvalue."defaultValue:s,isDeprecated:b!,deprecationReason:s
10+
__EnumValue: T:"OnepossiblevalueforagivenEnum.Enumvaluesareuniquevalues,notaplaceholderforastringornumericvalue.HoweveranEnumvalueisreturnedinaJSONresponseasastring."__EnumValue:name:s!,description:s,isDeprecated:b!,deprecationReason:s
11+
__Directive: T:"ADirectiveprovidesawaytodescribealternateruntimeexecutionandtypevalidationbehaviorinaGraphQLdocument.Insomecases,youneedtoprovideoptionstoalterGraphQL'sexecutionbehaviorinwaysfieldargumentswillnotsuffice,suchasconditionallyincludingorskippingafield.Directivesprovidethisbydescribingadditionalinformationtotheexecutor."__Directive:name:s!,description:s,locations:[__DirectiveLocation],args(includeDeprecated:b):[__InputValue],isRepeatable:b!
12+
__DirectiveLocation: E:"ADirectivecanbeadjacenttomanypartsoftheGraphQLlanguage,a__DirectiveLocationdescribesonesuchpossibleadjacencies."__DirectiveLocation:QUERY,MUTATION,SUBSCRIPTION,FIELD,FRAGMENT_DEFINITION,FRAGMENT_SPREAD,INLINE_FRAGMENT,VARIABLE_DEFINITION,SCHEMA,SCALAR,OBJECT,FIELD_DEFINITION,ARGUMENT_DEFINITION,INTERFACE,UNION,ENUM,ENUM_VALUE,INPUT_OBJECT,INPUT_FIELD_DEFINITION
13+
Int: i
14+
Float: f
15+
String: s
16+
Boolean: b
17+
ID: d
18+
DateTime: DateTime
19+
JSON: JSON
20+
Upload: Upload
21+
UserRole: E:UserRole:ADMIN,MODERATOR,USER,GUEST
22+
ContentStatus: E:ContentStatus:DRAFT,PUBLISHED,ARCHIVED,DELETED
23+
NotificationPriority: E:NotificationPriority:LOW,MEDIUM,HIGH,URGENT
24+
MediaType: E:MediaType:IMAGE,VIDEO,AUDIO,DOCUMENT
25+
Node: F:Node:id:d!,createdAt:DateTime!,updatedAt:DateTime!
26+
Content: F:Content:id:d!,title:s!,status:ContentStatus!,author:User!,metadata:JSON
27+
User: T:User<Node>:id:d!,createdAt:DateTime!,updatedAt:DateTime!,username:s!,email:s!,role:UserRole!,profile:UserProfile,posts:[Post],comments:[Comment],notifications:[Notification],preferences:UserPreferences!
28+
UserProfile: T:UserProfile:firstName:s,lastName:s,bio:s,avatar:Media,socialLinks:[SocialLink],location:Location
29+
Location: T:Location:country:s!,city:s,coordinates:Coordinates
30+
Coordinates: T:Coordinates:latitude:f!,longitude:f!
31+
SocialLink: T:SocialLink:platform:s!,url:s!,verified:b!
32+
Post: T:Post<Node,Content>:id:d!,createdAt:DateTime!,updatedAt:DateTime!,title:s!,content:s!,status:ContentStatus!,author:User!,metadata:JSON,comments:[Comment],media:[Media],tags:[Tag],analytics:PostAnalytics!
33+
Comment: T:Comment<Node>:id:d!,createdAt:DateTime!,updatedAt:DateTime!,content:s!,author:User!,post:Post!,parentComment:Comment,replies:[Comment],reactions:[Reaction]
34+
Media: T:Media:id:d!,type:MediaType!,url:s!,thumbnail:s,metadata:MediaMetadata!,uploader:User!
35+
MediaMetadata: T:MediaMetadata:size:i!,format:s!,dimensions:Dimensions,duration:i
36+
Dimensions: T:Dimensions:width:i!,height:i!
37+
Tag: T:Tag:id:d!,name:s!,slug:s!,description:s,posts:[Post]
38+
Reaction: T:Reaction:id:d!,type:s!,user:User!,comment:Comment!,createdAt:DateTime!
39+
Notification: T:Notification:id:d!,type:s!,priority:NotificationPriority!,message:s!,recipient:User!,read:b!,createdAt:DateTime!,metadata:JSON
40+
PostAnalytics: T:PostAnalytics:views:i!,likes:i!,shares:i!,comments:i!,engagement:f!,demographics:Demographics!
41+
Demographics: T:Demographics:ageGroups:[AgeGroup],locations:[LocationStats],devices:[DeviceStats]
42+
AgeGroup: T:AgeGroup:range:s!,percentage:f!
43+
LocationStats: T:LocationStats:country:s!,count:i!
44+
DeviceStats: T:DeviceStats:type:s!,count:i!
45+
UserPreferences: T:UserPreferences:theme:s!,oldTheme:s@D,language:s!,notifications:NotificationPreferences!,privacy:PrivacySettings!
46+
NotificationPreferences: T:NotificationPreferences:email:b!,push:b!,sms:b!,frequency:s!
47+
PrivacySettings: T:PrivacySettings:profileVisibility:s!,showEmail:b!,showLocation:b!
48+
CreateUserInput: I:CreateUserInput:username:s!,email:s!,password:s!,role:UserRole,profile:CreateUserProfileInput
49+
CreateUserProfileInput: I:CreateUserProfileInput:firstName:s,lastName:s,bio:s,location:CreateLocationInput
50+
CreateLocationInput: I:CreateLocationInput:country:s!,city:s,coordinates:CreateCoordinatesInput
51+
CreateCoordinatesInput: I:CreateCoordinatesInput:latitude:f!,longitude:f!
52+
CreatePostInput: I:CreatePostInput:title:s!,content:s!,status:ContentStatus,tags:[s],media:[Upload]
53+
UpdatePostInput: I:UpdatePostInput:title:s,content:s,status:ContentStatus,tags:[s]
54+
CreateCommentInput: I:CreateCommentInput:content:s!,postId:d!,parentCommentId:d
55+
NotificationFilter: I:NotificationFilter:priority:NotificationPriority,read:b,type:s,startDate:DateTime,endDate:DateTime
56+
Query: T:Query:node(id:d!):Node,user(id:d!):User,post(id:d!):Post,postsOld(filter:[d]):[Post]@D("Usepostsinstead"),posts(filter:PostFilter):[Post],comments(postId:d!):[Comment],notifications(filter:NotificationFilter):[Notification],search(query:s!):SearchResult!
57+
Mutation: T:Mutation:createUser(input:CreateUserInput!):User!,createPost(input:CreatePostInput!):Post!,updatePost(id:d!,input:UpdatePostInput!):Post!,createComment(input:CreateCommentInput!):Comment!,deletePost(id:d!):b!,uploadMedia(file:Upload!):Media!,updateUserPreferences(id:d!,preferences:UserPreferencesInput!):UserPreferences!
58+
Subscription: T:Subscription:postUpdated(id:d!):Post!,newComment(postId:d!):Comment!,notificationReceived(userId:d!):Notification!
59+
SearchResult: U:SearchResult:User,Post,Comment,Tag
60+
PostFilter: I:PostFilter:status:ContentStatus,authorId:d,tags:[s],dateRange:DateRangeInput
61+
DateRangeInput: I:DateRangeInput:start:DateTime!,end:DateTime!
62+
UserPreferencesInput: I:UserPreferencesInput:theme:s,language:s,notifications:NotificationPreferencesInput,privacy:PrivacySettingsInput
63+
NotificationPreferencesInput: I:NotificationPreferencesInput:email:b,push:b,sms:b,frequency:s
64+
PrivacySettingsInput: I:PrivacySettingsInput:profileVisibility:s,showEmail:b,showLocation:b

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ fn tool_description(
122122
minify: bool,
123123
) -> String {
124124
if minify {
125-
"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()
125+
"Get GraphQL type information - T=type,I=input,E=enum,U=union,F=interface;s=String,i=Int,f=Float,b=Boolean,d=ID;@D=deprecated;!=required,[]=list,<>=implements;".to_string()
126126
} else {
127127
format!(
128128
"Get information about a given GraphQL type defined in the schema. Instructions: Use this tool to explore the schema by providing specific type names. Start with the root query ({}) or mutation ({}) types to discover available fields. If the search tool is also available, use this tool first to get the fields, then use the search tool with relevant field return types and argument input types (ignore default GraphQL scalars) as search terms.",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ impl Search {
7777
format!(
7878
"Search a GraphQL schema for types matching the provided search terms. Returns complete type definitions including all related types needed to construct GraphQL operations. Instructions: If the introspect tool is also available, you can discover type names by using the introspect tool starting from the root Query or Mutation types. Avoid reusing previously searched terms for more efficient exploration.{}",
7979
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"
80+
" - T=type,I=input,E=enum,U=union,F=interface;s=String,i=Int,f=Float,b=Boolean,d=ID;@D=deprecated;!=required,[]=list,<>=implements"
8181
} else {
8282
""
8383
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type Post implements Node & Content {
2828

2929
type Query {
3030
user(id: ID!): User
31-
post(id: ID!): Post
31+
postsOld(filter: [ID!]): [Post!]! @deprecated(reason: "Use posts instead")
3232
posts(filter: PostFilter): [Post!]!
3333
}
3434

crates/apollo-mcp-server/src/introspection/tools/testdata/schema.graphql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ type DeviceStats {
191191

192192
type UserPreferences {
193193
theme: String!
194+
oldTheme: String @deprecated
194195
language: String!
195196
notifications: NotificationPreferences!
196197
privacy: PrivacySettings!
@@ -268,6 +269,7 @@ type Query {
268269
node(id: ID!): Node
269270
user(id: ID!): User
270271
post(id: ID!): Post
272+
postsOld(filter: [ID!]) : [Post!]! @deprecated(reason: "Use posts instead")
271273
posts(filter: PostFilter): [Post!]!
272274
comments(postId: ID!): [Comment!]!
273275
notifications(filter: NotificationFilter): [Notification!]!

docs/source/define-tools.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ Both the `introspect` and `search` tools support minification of their results t
188188

189189
- **Type prefixes**: `T=type`, `I=input`, `E=enum`, `U=union`, `F=interface`
190190
- **Scalar abbreviations**: `s=String`, `i=Int`, `f=Float`, `b=Boolean`, `d=ID`
191+
- **Directive abbreviations**: `@D=deprecated`
191192
- **Type modifiers**: `!=required`, `[]=list`, `<>=implements`
192193

193194
Example comparison:

0 commit comments

Comments
 (0)