Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changesets/fix_evan_minify_include_builtin_directives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Minify: Add support for deprecated directive - @esilverm PR #367

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.
73 changes: 69 additions & 4 deletions crates/apollo-mcp-server/src/introspection/minify.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use apollo_compiler::schema::{ExtendedType, Type};
use regex::Regex;
use std::sync::OnceLock;
use std::{collections::HashMap, sync::OnceLock};

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

// We should only minify directives that assist the LLM in understanding the schema. This included @deprecated
fn minify_directives(directives: &apollo_compiler::ast::DirectiveList) -> String {
let mut result = String::new();

static DIRECTIVES_TO_MINIFY: OnceLock<HashMap<&str, &str>> = OnceLock::new();
let directives_to_minify =
DIRECTIVES_TO_MINIFY.get_or_init(|| HashMap::from([("deprecated", "D")]));

for directive in directives.iter() {
if let Some(minified_name) = directives_to_minify.get(directive.name.as_str()) {
// Since we're only handling @deprecated right now we can just add the reason and minify it.
// We should handle this more generically in the future.
if !directive.arguments.is_empty()
&& let Some(reason) = directive
.arguments
.iter()
.find(|a| a.name == "reason")
.and_then(|a| a.value.as_str())
{
result.push_str(&format!(
"@{}(\"{}\")",
minified_name,
normalize_description(reason)
));
} else {
result.push_str(&format!("@{}", minified_name));
}
}
}
result
}

fn minify_fields(
fields: &apollo_compiler::collections::IndexMap<
apollo_compiler::Name,
Expand Down Expand Up @@ -99,6 +131,8 @@ fn minify_fields(
// Add field type
result.push(':');
result.push_str(&type_name(&field.ty));
result.push_str(&minify_directives(&field.directives));

result.push(',');
}

Expand Down Expand Up @@ -128,6 +162,7 @@ fn minify_input_fields(
result.push_str(field_name.as_str());
result.push(':');
result.push_str(&type_name(&field.ty));
result.push_str(&minify_directives(&field.directives));
result.push(',');
}

Expand All @@ -147,13 +182,19 @@ fn minify_arguments(
.map(|arg| {
if let Some(desc) = arg.description.as_ref() {
format!(
"\"{}\"{}:{}",
"\"{}\"{}:{}{}",
normalize_description(desc),
arg.name.as_str(),
type_name(&arg.ty)
type_name(&arg.ty),
minify_directives(&arg.directives)
)
} else {
format!("{}:{}", arg.name.as_str(), type_name(&arg.ty))
format!(
"{}:{}{}",
arg.name.as_str(),
type_name(&arg.ty),
minify_directives(&arg.directives)
)
}
})
.collect::<Vec<String>>()
Expand Down Expand Up @@ -211,3 +252,27 @@ fn normalize_description(desc: &str) -> String {
let re = WHITESPACE_PATTERN.get_or_init(|| Regex::new(r"\s+").expect("regex pattern compiles"));
re.replace_all(desc, "").to_string()
}

#[cfg(test)]
mod tests {
use super::*;

const TEST_SCHEMA: &str = include_str!("tools/testdata/schema.graphql");

#[test]
fn test_minify_schema() {
let schema = apollo_compiler::schema::Schema::parse(TEST_SCHEMA, "schema.graphql")
.expect("Failed to parse schema")
.validate()
.expect("Failed to validate schema");

let minified = schema
.types
.iter()
.map(|(_, type_)| format!("{}: {}", type_.name().as_str(), type_.minify()))
.collect::<Vec<String>>()
.join("\n");

insta::assert_snapshot!(minified);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
source: crates/apollo-mcp-server/src/introspection/minify.rs
expression: minified
---
__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]
__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
__TypeKind: E:"Anenumdescribingwhatkindoftypeagiven`__Type`is."__TypeKind:SCALAR,OBJECT,INTERFACE,UNION,ENUM,INPUT_OBJECT,LIST,NON_NULL
__Field: T:"ObjectandInterfacetypesaredescribedbyalistofFields,eachofwhichhasaname,potentiallyalistofarguments,andareturntype."__Field:name:s!,description:s,args(includeDeprecated:b):[__InputValue],type:__Type!,isDeprecated:b!,deprecationReason:s
__InputValue: T:"ArgumentsprovidedtoFieldsorDirectivesandtheinputfieldsofanInputObjectarerepresentedasInputValueswhichdescribetheirtypeandoptionallyadefaultvalue."__InputValue:name:s!,description:s,type:__Type!,"AGraphQL-formattedstringrepresentingthedefaultvalueforthisinputvalue."defaultValue:s,isDeprecated:b!,deprecationReason:s
__EnumValue: T:"OnepossiblevalueforagivenEnum.Enumvaluesareuniquevalues,notaplaceholderforastringornumericvalue.HoweveranEnumvalueisreturnedinaJSONresponseasastring."__EnumValue:name:s!,description:s,isDeprecated:b!,deprecationReason:s
__Directive: T:"ADirectiveprovidesawaytodescribealternateruntimeexecutionandtypevalidationbehaviorinaGraphQLdocument.Insomecases,youneedtoprovideoptionstoalterGraphQL'sexecutionbehaviorinwaysfieldargumentswillnotsuffice,suchasconditionallyincludingorskippingafield.Directivesprovidethisbydescribingadditionalinformationtotheexecutor."__Directive:name:s!,description:s,locations:[__DirectiveLocation],args(includeDeprecated:b):[__InputValue],isRepeatable:b!
__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
Int: i
Float: f
String: s
Boolean: b
ID: d
DateTime: DateTime
JSON: JSON
Upload: Upload
UserRole: E:UserRole:ADMIN,MODERATOR,USER,GUEST
ContentStatus: E:ContentStatus:DRAFT,PUBLISHED,ARCHIVED,DELETED
NotificationPriority: E:NotificationPriority:LOW,MEDIUM,HIGH,URGENT
MediaType: E:MediaType:IMAGE,VIDEO,AUDIO,DOCUMENT
Node: F:Node:id:d!,createdAt:DateTime!,updatedAt:DateTime!
Content: F:Content:id:d!,title:s!,status:ContentStatus!,author:User!,metadata:JSON
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!
UserProfile: T:UserProfile:firstName:s,lastName:s,bio:s,avatar:Media,socialLinks:[SocialLink],location:Location
Location: T:Location:country:s!,city:s,coordinates:Coordinates
Coordinates: T:Coordinates:latitude:f!,longitude:f!
SocialLink: T:SocialLink:platform:s!,url:s!,verified:b!
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!
Comment: T:Comment<Node>:id:d!,createdAt:DateTime!,updatedAt:DateTime!,content:s!,author:User!,post:Post!,parentComment:Comment,replies:[Comment],reactions:[Reaction]
Media: T:Media:id:d!,type:MediaType!,url:s!,thumbnail:s,metadata:MediaMetadata!,uploader:User!
MediaMetadata: T:MediaMetadata:size:i!,format:s!,dimensions:Dimensions,duration:i
Dimensions: T:Dimensions:width:i!,height:i!
Tag: T:Tag:id:d!,name:s!,slug:s!,description:s,posts:[Post]
Reaction: T:Reaction:id:d!,type:s!,user:User!,comment:Comment!,createdAt:DateTime!
Notification: T:Notification:id:d!,type:s!,priority:NotificationPriority!,message:s!,recipient:User!,read:b!,createdAt:DateTime!,metadata:JSON
PostAnalytics: T:PostAnalytics:views:i!,likes:i!,shares:i!,comments:i!,engagement:f!,demographics:Demographics!
Demographics: T:Demographics:ageGroups:[AgeGroup],locations:[LocationStats],devices:[DeviceStats]
AgeGroup: T:AgeGroup:range:s!,percentage:f!
LocationStats: T:LocationStats:country:s!,count:i!
DeviceStats: T:DeviceStats:type:s!,count:i!
UserPreferences: T:UserPreferences:theme:s!,oldTheme:s@D,language:s!,notifications:NotificationPreferences!,privacy:PrivacySettings!
NotificationPreferences: T:NotificationPreferences:email:b!,push:b!,sms:b!,frequency:s!
PrivacySettings: T:PrivacySettings:profileVisibility:s!,showEmail:b!,showLocation:b!
CreateUserInput: I:CreateUserInput:username:s!,email:s!,password:s!,role:UserRole,profile:CreateUserProfileInput
CreateUserProfileInput: I:CreateUserProfileInput:firstName:s,lastName:s,bio:s,location:CreateLocationInput
CreateLocationInput: I:CreateLocationInput:country:s!,city:s,coordinates:CreateCoordinatesInput
CreateCoordinatesInput: I:CreateCoordinatesInput:latitude:f!,longitude:f!
CreatePostInput: I:CreatePostInput:title:s!,content:s!,status:ContentStatus,tags:[s],media:[Upload]
UpdatePostInput: I:UpdatePostInput:title:s,content:s,status:ContentStatus,tags:[s]
CreateCommentInput: I:CreateCommentInput:content:s!,postId:d!,parentCommentId:d
NotificationFilter: I:NotificationFilter:priority:NotificationPriority,read:b,type:s,startDate:DateTime,endDate:DateTime
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!
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!
Subscription: T:Subscription:postUpdated(id:d!):Post!,newComment(postId:d!):Comment!,notificationReceived(userId:d!):Notification!
SearchResult: U:SearchResult:User,Post,Comment,Tag
PostFilter: I:PostFilter:status:ContentStatus,authorId:d,tags:[s],dateRange:DateRangeInput
DateRangeInput: I:DateRangeInput:start:DateTime!,end:DateTime!
UserPreferencesInput: I:UserPreferencesInput:theme:s,language:s,notifications:NotificationPreferencesInput,privacy:PrivacySettingsInput
NotificationPreferencesInput: I:NotificationPreferencesInput:email:b,push:b,sms:b,frequency:s
PrivacySettingsInput: I:PrivacySettingsInput:profileVisibility:s,showEmail:b,showLocation:b
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ fn tool_description(
minify: bool,
) -> String {
if minify {
"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()
"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()
} else {
format!(
"Get detailed information about types from the GraphQL schema.{}{}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl Search {
format!(
"Search a GraphQL schema{}",
if minify {
" - T=type,I=input,E=enum,U=union,F=interface;s=String,i=Int,f=Float,b=Boolean,d=ID;!=required,[]=list,<>=implements"
" - 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"
} else {
""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type Post implements Node & Content {

type Query {
user(id: ID!): User
post(id: ID!): Post
postsOld(filter: [ID!]): [Post!]! @deprecated(reason: "Use posts instead")
posts(filter: PostFilter): [Post!]!
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ type DeviceStats {

type UserPreferences {
theme: String!
oldTheme: String @deprecated
language: String!
notifications: NotificationPreferences!
privacy: PrivacySettings!
Expand Down Expand Up @@ -268,6 +269,7 @@ type Query {
node(id: ID!): Node
user(id: ID!): User
post(id: ID!): Post
postsOld(filter: [ID!]) : [Post!]! @deprecated(reason: "Use posts instead")
posts(filter: PostFilter): [Post!]!
comments(postId: ID!): [Comment!]!
notifications(filter: NotificationFilter): [Notification!]!
Expand Down
1 change: 1 addition & 0 deletions docs/source/define-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@

- **Type prefixes**: `T=type`, `I=input`, `E=enum`, `U=union`, `F=interface`
- **Scalar abbreviations**: `s=String`, `i=Int`, `f=Float`, `b=Boolean`, `d=ID`
- **Directive abbreviations**: `@D=deprecated`

Check warning on line 159 in docs/source/define-tools.mdx

View check run for this annotation

Apollo Librarian / AI Style Review

docs/source/define-tools.mdx#L159

The use of `=` to denote an abbreviation is unclear. Use plain language to explain the relationship between the two terms for better readability. ```suggestion - **Directive abbreviations**: <code>@d</code> for <code>@deprecated</code> ```
- **Type modifiers**: `!=required`, `[]=list`, `<>=implements`

Example comparison:
Expand Down