#
tokens: 28129/50000 2/187 files (page 6/8)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 6 of 8. Use http://codebase.md/apollographql/apollo-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cargo
│   └── config.toml
├── .changesets
│   └── README.md
├── .envrc
├── .github
│   ├── CODEOWNERS
│   ├── renovate.json5
│   └── workflows
│       ├── canary-release.yml
│       ├── ci.yml
│       ├── prep-release.yml
│       ├── release-bins.yml
│       ├── release-container.yml
│       ├── sync-develop.yml
│       └── verify-changeset.yml
├── .gitignore
├── .idea
│   └── runConfigurations
│       ├── clippy.xml
│       ├── format___test___clippy.xml
│       ├── format.xml
│       ├── Run_spacedevs.xml
│       └── Test_apollo_mcp_server.xml
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   ├── settings.json
│   └── tasks.json
├── apollo.config.json
├── Cargo.lock
├── Cargo.toml
├── CHANGELOG_SECTION.md
├── CHANGELOG.md
├── clippy.toml
├── codecov.yml
├── CONTRIBUTING.md
├── crates
│   ├── apollo-mcp-registry
│   │   ├── Cargo.toml
│   │   └── src
│   │       ├── files.rs
│   │       ├── lib.rs
│   │       ├── logging.rs
│   │       ├── platform_api
│   │       │   ├── operation_collections
│   │       │   │   ├── collection_poller.rs
│   │       │   │   ├── error.rs
│   │       │   │   ├── event.rs
│   │       │   │   └── operation_collections.graphql
│   │       │   ├── operation_collections.rs
│   │       │   └── platform-api.graphql
│   │       ├── platform_api.rs
│   │       ├── testdata
│   │       │   ├── minimal_supergraph.graphql
│   │       │   └── supergraph.graphql
│   │       ├── uplink
│   │       │   ├── persisted_queries
│   │       │   │   ├── event.rs
│   │       │   │   ├── manifest_poller.rs
│   │       │   │   ├── manifest.rs
│   │       │   │   └── persisted_queries_manifest_query.graphql
│   │       │   ├── persisted_queries.rs
│   │       │   ├── schema
│   │       │   │   ├── event.rs
│   │       │   │   ├── schema_query.graphql
│   │       │   │   └── schema_stream.rs
│   │       │   ├── schema.rs
│   │       │   ├── snapshots
│   │       │   │   ├── apollo_mcp_registry__uplink__schema__tests__schema_by_url_all_fail@logs.snap
│   │       │   │   ├── apollo_mcp_registry__uplink__schema__tests__schema_by_url_fallback@logs.snap
│   │       │   │   └── apollo_mcp_registry__uplink__schema__tests__schema_by_url@logs.snap
│   │       │   └── uplink.graphql
│   │       └── uplink.rs
│   ├── apollo-mcp-server
│   │   ├── build.rs
│   │   ├── Cargo.toml
│   │   ├── src
│   │   │   ├── auth
│   │   │   │   ├── networked_token_validator.rs
│   │   │   │   ├── protected_resource.rs
│   │   │   │   ├── valid_token.rs
│   │   │   │   └── www_authenticate.rs
│   │   │   ├── auth.rs
│   │   │   ├── config_schema.rs
│   │   │   ├── cors.rs
│   │   │   ├── custom_scalar_map.rs
│   │   │   ├── errors.rs
│   │   │   ├── event.rs
│   │   │   ├── explorer.rs
│   │   │   ├── graphql.rs
│   │   │   ├── headers.rs
│   │   │   ├── health.rs
│   │   │   ├── introspection
│   │   │   │   ├── minify.rs
│   │   │   │   ├── snapshots
│   │   │   │   │   └── apollo_mcp_server__introspection__minify__tests__minify_schema.snap
│   │   │   │   ├── tools
│   │   │   │   │   ├── execute.rs
│   │   │   │   │   ├── introspect.rs
│   │   │   │   │   ├── search.rs
│   │   │   │   │   ├── snapshots
│   │   │   │   │   │   └── apollo_mcp_server__introspection__tools__search__tests__search_tool.snap
│   │   │   │   │   ├── testdata
│   │   │   │   │   │   └── schema.graphql
│   │   │   │   │   └── validate.rs
│   │   │   │   └── tools.rs
│   │   │   ├── introspection.rs
│   │   │   ├── json_schema.rs
│   │   │   ├── lib.rs
│   │   │   ├── main.rs
│   │   │   ├── meter.rs
│   │   │   ├── operations
│   │   │   │   ├── mutation_mode.rs
│   │   │   │   ├── operation_source.rs
│   │   │   │   ├── operation.rs
│   │   │   │   ├── raw_operation.rs
│   │   │   │   ├── schema_walker
│   │   │   │   │   ├── name.rs
│   │   │   │   │   └── type.rs
│   │   │   │   └── schema_walker.rs
│   │   │   ├── operations.rs
│   │   │   ├── runtime
│   │   │   │   ├── config.rs
│   │   │   │   ├── endpoint.rs
│   │   │   │   ├── filtering_exporter.rs
│   │   │   │   ├── graphos.rs
│   │   │   │   ├── introspection.rs
│   │   │   │   ├── logging
│   │   │   │   │   ├── defaults.rs
│   │   │   │   │   ├── log_rotation_kind.rs
│   │   │   │   │   └── parsers.rs
│   │   │   │   ├── logging.rs
│   │   │   │   ├── operation_source.rs
│   │   │   │   ├── overrides.rs
│   │   │   │   ├── schema_source.rs
│   │   │   │   ├── schemas.rs
│   │   │   │   ├── telemetry
│   │   │   │   │   └── sampler.rs
│   │   │   │   └── telemetry.rs
│   │   │   ├── runtime.rs
│   │   │   ├── sanitize.rs
│   │   │   ├── schema_tree_shake.rs
│   │   │   ├── server
│   │   │   │   ├── states
│   │   │   │   │   ├── configuring.rs
│   │   │   │   │   ├── operations_configured.rs
│   │   │   │   │   ├── running.rs
│   │   │   │   │   ├── schema_configured.rs
│   │   │   │   │   └── starting.rs
│   │   │   │   └── states.rs
│   │   │   ├── server.rs
│   │   │   └── telemetry_attributes.rs
│   │   └── telemetry.toml
│   └── apollo-schema-index
│       ├── Cargo.toml
│       └── src
│           ├── error.rs
│           ├── lib.rs
│           ├── path.rs
│           ├── snapshots
│           │   ├── apollo_schema_index__tests__search.snap
│           │   └── apollo_schema_index__traverse__tests__schema_traverse.snap
│           ├── testdata
│           │   └── schema.graphql
│           └── traverse.rs
├── docs
│   └── source
│       ├── _sidebar.yaml
│       ├── auth.mdx
│       ├── best-practices.mdx
│       ├── config-file.mdx
│       ├── cors.mdx
│       ├── custom-scalars.mdx
│       ├── debugging.mdx
│       ├── define-tools.mdx
│       ├── deploy.mdx
│       ├── guides
│       │   └── auth-auth0.mdx
│       ├── health-checks.mdx
│       ├── images
│       │   ├── auth0-permissions-enable.png
│       │   ├── mcp-getstarted-inspector-http.jpg
│       │   └── mcp-getstarted-inspector-stdio.jpg
│       ├── index.mdx
│       ├── licensing.mdx
│       ├── limitations.mdx
│       ├── quickstart.mdx
│       ├── run.mdx
│       └── telemetry.mdx
├── e2e
│   └── mcp-server-tester
│       ├── local-operations
│       │   ├── api.graphql
│       │   ├── config.yaml
│       │   ├── operations
│       │   │   ├── ExploreCelestialBodies.graphql
│       │   │   ├── GetAstronautDetails.graphql
│       │   │   ├── GetAstronautsCurrentlyInSpace.graphql
│       │   │   └── SearchUpcomingLaunches.graphql
│       │   └── tool-tests.yaml
│       ├── pq-manifest
│       │   ├── api.graphql
│       │   ├── apollo.json
│       │   ├── config.yaml
│       │   └── tool-tests.yaml
│       ├── run_tests.sh
│       └── server-config.template.json
├── flake.lock
├── flake.nix
├── graphql
│   ├── TheSpaceDevs
│   │   ├── .vscode
│   │   │   ├── extensions.json
│   │   │   └── tasks.json
│   │   ├── api.graphql
│   │   ├── apollo.config.json
│   │   ├── config.yaml
│   │   ├── operations
│   │   │   ├── ExploreCelestialBodies.graphql
│   │   │   ├── GetAstronautDetails.graphql
│   │   │   ├── GetAstronautsCurrentlyInSpace.graphql
│   │   │   └── SearchUpcomingLaunches.graphql
│   │   ├── persisted_queries
│   │   │   └── apollo.json
│   │   ├── persisted_queries.config.json
│   │   ├── README.md
│   │   └── supergraph.yaml
│   └── weather
│       ├── api.graphql
│       ├── config.yaml
│       ├── operations
│       │   ├── alerts.graphql
│       │   ├── all.graphql
│       │   └── forecast.graphql
│       ├── persisted_queries
│       │   └── apollo.json
│       ├── supergraph.graphql
│       ├── supergraph.yaml
│       └── weather.graphql
├── LICENSE
├── macos-entitlements.plist
├── nix
│   ├── apollo-mcp.nix
│   ├── cargo-zigbuild.patch
│   ├── mcp-server-tools
│   │   ├── default.nix
│   │   ├── node-generated
│   │   │   ├── default.nix
│   │   │   ├── node-env.nix
│   │   │   └── node-packages.nix
│   │   ├── node-mcp-servers.json
│   │   └── README.md
│   └── mcphost.nix
├── README.md
├── rust-toolchain.toml
├── scripts
│   ├── nix
│   │   └── install.sh
│   └── windows
│       └── install.ps1
└── xtask
    ├── Cargo.lock
    ├── Cargo.toml
    └── src
        ├── commands
        │   ├── changeset
        │   │   ├── matching_pull_request.graphql
        │   │   ├── matching_pull_request.rs
        │   │   ├── mod.rs
        │   │   ├── scalars.rs
        │   │   └── snapshots
        │   │       ├── xtask__commands__changeset__tests__it_templatizes_with_multiple_issues_in_title_and_multiple_prs_in_footer.snap
        │   │       ├── xtask__commands__changeset__tests__it_templatizes_with_multiple_issues_in_title.snap
        │   │       ├── xtask__commands__changeset__tests__it_templatizes_with_multiple_prs_in_footer.snap
        │   │       ├── xtask__commands__changeset__tests__it_templatizes_with_neither_issues_or_prs.snap
        │   │       ├── xtask__commands__changeset__tests__it_templatizes_with_prs_in_title_when_empty_issues.snap
        │   │       └── xtask__commands__changeset__tests__it_templatizes_without_prs_in_title_when_issues_present.snap
        │   └── mod.rs
        ├── lib.rs
        └── main.rs
```

# Files

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Changelog
  2 | 
  3 | All notable changes to this project will be documented in this file.
  4 | 
  5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
  6 | 
  7 | # [1.1.1] - 2025-10-21
  8 | 
  9 | ## 🐛 Fixes
 10 | 
 11 | ### fix docker image ignoring port setting - @DaleSeo PR #467
 12 | 
 13 | The Docker image had `APOLLO_MCP_TRANSPORT__PORT=8000` baked in as an environment variable in `flake.nix`. Since environment variables take precedence over config file settings (by design in our config loading logic), users are unable to override the port in their `config.yaml` when running the Docker container.
 14 | 
 15 | 
 16 | 
 17 | # [1.1.0] - 2025-10-16
 18 | 
 19 | ## ❗ BREAKING ❗
 20 | 
 21 | ### Change default port from 5000 to 8000 - @DaleSeo PR #417
 22 | 
 23 | The default server port has been changed from `5000` to `8000` to avoid conflicts with common development tools and services that typically use port 5000 (such as macOS AirPlay, Flask development servers, and other local services).
 24 | 
 25 | **Migration**: If you were relying on the default port 5000, you can continue using it by explicitly setting the port in your configuration file or command line arguments.
 26 | 
 27 | - Before 
 28 | 
 29 | ```yaml
 30 | transport:
 31 |   type: streamable_http
 32 | ```
 33 | 
 34 | - After
 35 | 
 36 | ```yaml
 37 | transport:
 38 |   type: streamable_http
 39 |   port: 5000
 40 | ```
 41 | 
 42 | ## 🚀 Features
 43 | 
 44 | ### feat: Add configuration option for metric temporality - @swcollard PR #413
 45 | 
 46 | Creates a new configuration option for telemetry to set the Metric temporality to either Cumulative (default) or Delta.
 47 | 
 48 | * Cumulative - The metric value will be the overall value since the start of the measurement.
 49 | * Delta - The metric will be the difference in the measurement since the last time it was reported.
 50 | 
 51 | Some observability  vendors require that one is used over the other so we want to support the configuration in the MCP Server.
 52 | 
 53 | ### Add support for forwarding headers from MCP clients to GraphQL APIs - @DaleSeo PR #428
 54 | 
 55 | Adds opt-in support for dynamic header forwarding, which enables metadata for A/B testing, feature flagging, geo information from CDNs, or internal instrumentation to be sent from MCP clients to downstream GraphQL APIs. It automatically blocks hop-by-hop headers according to the guidelines in [RFC 7230, section 6.1](https://datatracker.ietf.org/doc/html/rfc7230#section-6.1), and it only works with the Streamable HTTP transport.
 56 | 
 57 | You can configure using the `forward_headers` setting:
 58 | 
 59 | ```yaml
 60 | forward_headers:
 61 |   - x-tenant-id
 62 |   - x-experiment-id
 63 |   - x-geo-country
 64 | ```
 65 | 
 66 | Please note that this feature is not intended for passing through credentials as documented in the best practices page.
 67 | 
 68 | ### feat: Add mcp-session-id header to HTTP request trace attributes - @swcollard PR #421
 69 | 
 70 | Includes the value of the [Mcp-Session-Id](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#session-management) HTTP header as an attribute of the trace for HTTP requests to the MCP Server
 71 | 
 72 | ## 🐛 Fixes
 73 | 
 74 | ### Fix compatibility issue with VSCode/Copilot - @DaleSeo PR #447
 75 | 
 76 | 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.
 77 | 
 78 | ## 🛠 Maintenance
 79 | 
 80 | ### Update rmcp sdk to version 0.8.x - @swcollard PR #433 
 81 | 
 82 | Bumping the Rust MCP SDK version used in this server up to 0.8.x
 83 | 
 84 | ### chore: Only initialize a single HTTP client for graphql requests - @swcollard PR #412
 85 | 
 86 | Currently the MCP Server spins up a new HTTP client every time it wants to make a request to the downstream graphql endpoint. This change creates a static reqwest client that gets initialized using LazyLock and reused on each graphql request.
 87 | 
 88 | This change is based on the suggestion from the reqwest [documentation](https://docs.rs/reqwest/latest/reqwest/struct.Client.html)
 89 | > "The Client holds a connection pool internally, so it is advised that you create one and reuse it."
 90 | 
 91 | 
 92 | 
 93 | # [1.0.0] - 2025-10-01
 94 | 
 95 | # Apollo MCP Server 1.0 Release Notes
 96 | 
 97 | Apollo MCP Server 1.0 marks the **General Availability (GA)** milestone, delivering a production-ready Model Context Protocol server that seamlessly bridges GraphQL APIs with AI applications. This release transforms how AI agents interact with GraphQL APIs through standardized MCP tools, enabling natural language access to your GraphQL operations.
 98 | 
 99 | ## 🎯 GA Highlights
100 | 
101 | ### **Production-Ready MCP Protocol Implementation**
102 | 
103 | Apollo MCP Server 1.0 provides full compliance with the [MCP specification](https://modelcontextprotocol.io/specification/2025-06-18), enabling AI applications to discover and invoke GraphQL operations through standardized protocols. The server acts as a translation layer, converting GraphQL operations into MCP tools that AI models can execute through natural language requests.
104 | 
105 | **Key Benefits:**
106 | 
107 | - **Standardized AI Integration**: No more custom API bridges - use the industry-standard MCP protocol
108 | - **Automatic Tool Discovery**: AI agents automatically discover available GraphQL operations as MCP tools
109 | - **Type-Safe Execution**: All operations are validated against your GraphQL schema before execution
110 | - **Enterprise-Ready**: Full OAuth 2.1 authentication and comprehensive observability
111 | 
112 | ### **🚀 Multi-Transport Architecture**
113 | 
114 | Flexible communication options for every deployment scenario:
115 | 
116 | - **stdio**: Perfect for local development and debugging with MCP Inspector
117 | - **Streamable HTTP**: Production-grade transport with load balancer support and concurrent connections
118 | 
119 | All transports maintain full MCP protocol compliance while optimizing for specific use cases.
120 | 
121 | ### **🔧 Advanced GraphQL Integration**
122 | 
123 | **Custom Scalar Support**: Seamlessly handle specialized types like `DateTime`, `UUID`, and domain-specific scalars with automatic JSON Schema mapping.
124 | 
125 | **Mutation Controls**: Fine-grained security controls to prevent unintended data changes:
126 | 
127 | - `all`: Enable all mutations (default)
128 | - `none`: Disable all mutations for read-only access
129 | - `allowlist`: Only allow specific mutations
130 | 
131 | ### **📊 Flexible Schema & Operation Management**
132 | 
133 | **Dual Schema Sources:**
134 | 
135 | - **Local Files**: Direct schema control for development and offline scenarios
136 | - **Apollo GraphOS**: Centralized schema management with automatic updates via uplink integration
137 | 
138 | **Multiple Operation Sources:**
139 | 
140 | - **Local Statement Files**: Hot-reloading `.graphql` files for rapid development
141 | - **Persisted Query Manifests**: Security-focused pre-approved operation execution
142 | - **GraphOS Operation Collections**: Centrally managed operations with automatic polling
143 | - **GraphOS Persisted Queries**: Enterprise-grade operation management
144 | 
145 | ### **🤖 AI-Optimized Introspection Tools**
146 | 
147 | **Core Tools:**
148 | 
149 | - **`introspect`**: Comprehensive schema exploration with AI-friendly formatting
150 | - **`execute`**: Safe dynamic operation execution with proper error handling
151 | - **`validate`**: Operation validation without execution to prevent side effects
152 | - **`search`**: Semantic schema search to efficiently find relevant types and fields
153 | 
154 | **AI Optimizations:**
155 | 
156 | - **Minified Output**: Configurable minification reduces context window usage by 30%+ while preserving essential information
157 | - **Semantic Search**: Natural language schema exploration with ranked results
158 | 
159 | ### **⚙️ Configuration-Driven Architecture**
160 | 
161 | **YAML Configuration**: Replace complex command-line arguments with structured, version-controllable configuration files.
162 | 
163 | **Environment Variable Overrides**: Seamless environment-specific customization using the `APOLLO_MCP_` prefix convention.
164 | 
165 | **Comprehensive Validation**: Clear error messages and sensible defaults for rapid deployment.
166 | 
167 | ### **🔐 Enterprise Security & Observability**
168 | 
169 | **OAuth 2.1 Authentication**: Production-ready authentication supporting major identity providers:
170 | 
171 | - Auth0, WorkOS, Keycloak, Okta
172 | - JWT token validation with audience and scope enforcement
173 | - OIDC discovery for automatic provider configuration
174 | 
175 | **Health Monitoring**: Kubernetes-ready health checks with configurable liveness and readiness probes.
176 | 
177 | **OpenTelemetry Integration**: Comprehensive observability with traces, metrics, and events:
178 | 
179 | - Operation-level performance tracking
180 | - Semantic conventions for HTTP servers when using the Streamable HTTP transport.
181 | - OTLP export to any OpenTelemetry-compatible collector
182 | - Integration with existing monitoring infrastructure
183 | 
184 | **CORS Support**: Enable browser-based MCP clients with comprehensive Cross-Origin Resource Sharing support following Apollo Router patterns.
185 | 
186 | ## 🐛 Fixes
187 | 
188 | ### fix: remove verbose logging - @swcollard PR #401
189 | 
190 | The tracing-subscriber crate we are using to create logs does not have a configuration to exclude the span name and attributes from the log line. This led to rather verbose logs on app startup which would dump the full operation object into the logs before the actual log line.
191 | 
192 | This change strips the attributes from the top level spans so that we still have telemetry and tracing during this important work the server is doing, but they don't make it into the logs. The relevant details are provided in child spans after the operation has been parsed so we aren't losing any information other than a large json blob in the top level trace of generating Tools from GraphQL Operations.
193 | 
194 | ## 🛠 Maintenance
195 | 
196 | ### deps: update rust to v1.90.0 - @DaleSeo PR #387
197 | 
198 | Updates the Rust version to v1.90.0
199 | 
200 | # [0.9.0] - 2025-09-24
201 | 
202 | ## 🚀 Features
203 | 
204 | ### Prototype OpenTelemetry Traces in MCP Server - @swcollard PR #274
205 | 
206 | Pulls in new crates and SDKs for prototyping instrumenting the Apollo MCP Server with Open Telemetry Traces.
207 | 
208 | - Adds new rust crates to support OTel
209 | - Annotates excecute and call_tool functions with trace macro
210 | - Adds Axum and Tower middleware's for OTel tracing
211 | - Refactors Logging so that all the tracing_subscribers are set together in a single module.
212 | 
213 | ### Add CORS support - @DaleSeo PR #362
214 | 
215 | This PR implements comprehensive CORS support for Apollo MCP Server to enable web-based MCP clients to connect without CORS errors. The implementation and configuration draw heavily from the Router's approach. Similar to other features like health checks and telemetry, CORS is supported only for the StreamableHttp transport, making it a top-level configuration.
216 | 
217 | ### Enhance tool descriptions - @DaleSeo PR #350
218 | 
219 | This PR enhances the descriptions of the introspect and search tools to offer clearer guidance for AI models on efficient GraphQL schema exploration patterns.
220 | 
221 | ### Telemetry: Trace operations and auth - @swcollard PR #375
222 | 
223 | - Adds traces for the MCP server generating Tools from Operations and performing authorization
224 | - Includes the HTTP status code to the top level HTTP trace
225 | 
226 | ### Implement metrics for mcp tool and operation counts and durations - @swcollard PR #297
227 | 
228 | This PR adds metrics to count and measure request duration to events throughout the MCP server
229 | 
230 | - apollo.mcp.operation.duration
231 | - apollo.mcp.operation.count
232 | - apollo.mcp.tool.duration
233 | - apollo.mcp.tool.count
234 | - apollo.mcp.initialize.count
235 | - apollo.mcp.list_tools.count
236 | - apollo.mcp.get_info.count
237 | 
238 | ### Adding ability to omit attributes for traces and metrics - @alocay PR #358
239 | 
240 | Adding ability to configure which attributes are omitted from telemetry traces and metrics.
241 | 
242 | 1. Using a Rust build script (`build.rs`) to auto-generate telemetry attribute code based on the data found in `telemetry.toml`.
243 | 2. Utilizing an enum for attributes so typos in the config file raise an error.
244 | 3. Omitting trace attributes by filtering it out in a custom exporter.
245 | 4. Omitting metric attributes by indicating which attributes are allowed via a view.
246 | 5. Created `telemetry_attributes.rs` to map `TelemetryAttribute` enum to a OTEL `Key`.
247 | 
248 | The `telemetry.toml` file includes attributes (both for metrics and traces) as well as list of metrics gathered. An example would look like the following:
249 | 
250 | ```
251 | [attributes.apollo.mcp]
252 | my_attribute = "Some attribute info"
253 | 
254 | [metrics.apollo.mcp]
255 | some.count = "Some metric count info"
256 | ```
257 | 
258 | This would generate a file that looks like the following:
259 | 
260 | ```
261 | /// All TelemetryAttribute values
262 | pub const ALL_ATTRS: &[TelemetryAttribute; 1usize] = &[
263 |     TelemetryAttribute::MyAttribute
264 | ];
265 | #[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema,, Clone, Eq, PartialEq, Hash, Copy)]
266 | pub enum TelemetryAttribute {
267 |     ///Some attribute info
268 |     #[serde(alias = "my_attribute")]
269 |     MyAttribute,
270 | }
271 | impl TelemetryAttribute {
272 |     /// Supported telemetry attribute (tags) values
273 |     pub const fn as_str(&self) -> &'static str {
274 |         match self {
275 |             TelemetryAttribute::MyAttribute => "apollo.mcp.my_attribute",
276 |         }
277 |     }
278 | }
279 | #[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema,, Clone, Eq, PartialEq, Hash, Copy)]
280 | pub enum TelemetryMetric {
281 |     ///Some metric count info
282 |     #[serde(alias = "some.count")]
283 |     SomeCount,
284 | }
285 | impl TelemetryMetric {
286 |     /// Converts TelemetryMetric to &str
287 |     pub const fn as_str(&self) -> &'static str {
288 |         match self {
289 |             TelemetryMetric::SomeCount => "apollo.mcp.some.count",
290 |         }
291 |     }
292 | }
293 | ```
294 | 
295 | An example configuration that omits `tool_name` attribute for metrics and `request_id` for tracing would look like the following:
296 | 
297 | ```
298 | telemetry:
299 |   exporters:
300 |     metrics:
301 |       otlp:
302 |         endpoint: "http://localhost:4317"
303 |         protocol: "grpc"
304 |       omitted_attributes:
305 |         - tool_name
306 |     tracing:
307 |       otlp:
308 |         endpoint: "http://localhost:4317"
309 |         protocol: "grpc"
310 |       omitted_attributes:
311 |         - request_id
312 | ```
313 | 
314 | ### Adding config option for trace sampling - @alocay PR #366
315 | 
316 | Adding configuration option to sample traces. Can use the following options:
317 | 
318 | 1. Ratio based samples (ratio >= 1 is always sample)
319 | 2. Always on
320 | 3. Always off
321 | 
322 | Defaults to always on if not provided.
323 | 
324 | ## 🐛 Fixes
325 | 
326 | ### Update SDL handling in sdl_to_api_schema function - @lennyburdette PR #365
327 | 
328 | Loads supergraph schemas using a function that supports various features, including Apollo Connectors. When supergraph loading failed, it would load it as a standard GraphQL schema, which reveals Federation query planning directives in when using the `search` and `introspection` tools.
329 | 
330 | ### Include the cargo feature and TraceContextPropagator to send otel headers downstream - @swcollard PR #307
331 | 
332 | Inside the reqwest middleware, if the global text_map_propagator is not set, it will no op and not send the traceparent and tracestate headers to the Router. Adding this is needed to correlate traces from the mcp server to the router or other downstream APIs
333 | 
334 | ### Add support for deprecated directive - @esilverm PR #367
335 | 
336 | 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.
337 | 
338 | ## 📃 Configuration
339 | 
340 | ### Add basic config file options to otel telemetry - @swcollard PR #330
341 | 
342 | Adds new Configuration options for setting up configuration beyond the standard OTEL environment variables needed before.
343 | 
344 | - Renames trace->telemetry
345 | - Adds OTLP options for metrics and tracing to choose grpc or http upload protocols and setting the endpoints
346 | - This configuration is all optional, so by default nothing will be logged
347 | 
348 | ### Disable statefulness to fix initialize race condition - @swcollard PR #351
349 | 
350 | We've been seeing errors with state and session handling in the MCP Server. Whether that is requests being sent before the initialized notification is processed. Or running a fleet of MCP Server pods behind a round robin load balancer. A new configuration option under the streamable_http transport `stateful_mode`, allows disabling session handling which appears to fix the race condition issue.
351 | 
352 | ## 🛠 Maintenance
353 | 
354 | ### Add tests for server event and SupergraphSdlQuery - @DaleSeo PR #347
355 | 
356 | This PR adds tests for some uncovered parts of the codebase to check the Codecov integration.
357 | 
358 | ### Fix version on mcp server tester - @alocay PR #374
359 | 
360 | Add a specific version when calling the mcp-server-tester for e2e tests. The current latest (1.4.1) as an issue so to avoid problems now and in the future updating the test script to invoke the testing tool via specific version.
361 | 
362 | # [0.8.0] - 2025-09-12
363 | 
364 | ## 🚀 Features
365 | 
366 | ### feat: Configuration for disabling authorization token passthrough - @swcollard PR #336
367 | 
368 | A new optional new MCP Server configuration parameter, `transport.auth.disable_auth_token_passthrough`, which is `false` by default, that when true, will no longer pass through validated Auth tokens to the GraphQL API.
369 | 
370 | ## 🛠 Maintenance
371 | 
372 | ### Configure Codecov with coverage targets - @DaleSeo PR #337
373 | 
374 | This PR adds `codecov.yml` to set up Codecov with specific coverage targets and quality standards. It helps define clear expectations for code quality. It also includes some documentation about code coverage in `CONTRIBUTING.md` and adds the Codecov badge to `README.md`.
375 | 
376 | ### Implement Test Coverage Measurement and Reporting - @DaleSeo PR #335
377 | 
378 | This PR adds the bare minimum for code coverage reporting using [cargo-llvm-cov](https://crates.io/crates/cargo-llvm-cov) and integrates with [Codecov](https://www.codecov.io/). It adds a new `coverage` job to the CI workflow that generates and uploads coverage reporting in parallel with existing tests. The setup mirrors that of Router, except it uses `nextest` instead of the built-in test runner and CircleCI instead of GitHub Actions.
379 | 
380 | ### chore: update RMCP dependency ([328](https://github.com/apollographql/apollo-mcp-server/issues/328))
381 | 
382 | Update the RMCP dependency to the latest version, pulling in newer specification changes.
383 | 
384 | ### ci: Pin stable rust version ([Issue #287](https://github.com/apollographql/apollo-mcp-server/issues/287))
385 | 
386 | Pins the stable version of Rust to the current latest version to ensure backwards compatibility with future versions.
387 | 
388 | # [0.7.5] - 2025-09-03
389 | 
390 | ## 🐛 Fixes
391 | 
392 | ### fix: Validate ExecutableDocument in validate tool - @swcollard PR #329
393 | 
394 | Contains fixes for https://github.com/apollographql/apollo-mcp-server/issues/327
395 | 
396 | The validate tool was parsing the operation passed in to it against the schema but it wasn't performing the validate function on the ExecutableDocument returned by the Parser. This led to cases where missing required arguments were not caught by the Tool.
397 | 
398 | This change also updates the input schema to the execute tool to make it more clear to the LLM that it needs to provide a valid JSON object
399 | 
400 | ## 🛠 Maintenance
401 | 
402 | ### test: adding a basic manual e2e test for mcp server - @alocay PR #320
403 | 
404 | Adding some basic e2e tests using [mcp-server-tester](https://github.com/steviec/mcp-server-tester). Currently, the tool does not always exit (ctrl+c is sometimes needed) so this should be run manually.
405 | 
406 | ### How to run tests?
407 | 
408 | Added a script `run_tests.sh` (may need to run `chmod +x` to run it) to run tests. Basic usage found via `./run_tests.sh -h`. The script does the following:
409 | 
410 | 1. Builds test/config yaml paths and verifies the files exist.
411 | 2. Checks if release `apollo-mcp-server` binary exists. If not, it builds the binary via `cargo build --release`.
412 | 3. Reads in the template file (used by `mcp-server-tester`) and replaces all `<test-dir>` placeholders with the test directory value. Generates this test server config file and places it in a temp location.
413 | 4. Invokes the `mcp-server-tester` via `npx`.
414 | 5. On script exit the generated config is cleaned up.
415 | 
416 | ### Example run:
417 | 
418 | To run the tests for `local-operations` simply run `./run_tests.sh local-operations`
419 | 
420 | ### Update snapshot format - @DaleSeo PR #313
421 | 
422 | Updates all inline snapshots in the codebase to ensure they are consistent with the latest insta format.
423 | 
424 | ### Hardcoded version strings in tests - @DaleSeo PR #305
425 | 
426 | The GraphQL tests have hardcoded version strings that we need to update manually each time we release a new version. Since this isn't included in the release checklist, it's easy to miss it and only notice the test failures later.
427 | 
428 | # [0.7.4] - 2025-08-27
429 | 
430 | ## 🐛 Fixes
431 | 
432 | ### fix: Add missing token propagation for execute tool - @DaleSeo PR #298
433 | 
434 | The execute tool is not forwarding JWT authentication tokens to upstream GraphQL endpoints, causing authentication failures when using this tool with protected APIs. This PR adds missing token propagation for execute tool.
435 | 
436 | # [0.7.3] - 2025-08-25
437 | 
438 | ## 🐛 Fixes
439 | 
440 | ### fix: generate openAI-compatible json schemas for list types - @DaleSeo PR #272
441 | 
442 | The MCP server is generating JSON schemas that don't match OpenAI's function calling specification. It puts `oneOf` at the array level instead of using `items` to define the JSON schemas for the GraphQL list types. While some other LLMs are more flexible about this, it technically violates the [JSON Schema specification](https://json-schema.org/understanding-json-schema/reference/array) that OpenAI strictly follows.
443 | 
444 | This PR updates the list type handling logic to move `oneOf` inside `items` for GraphQL list types.
445 | 
446 | # [0.7.2] - 2025-08-19
447 | 
448 | ## 🚀 Features
449 | 
450 | ### Prevent server restarts while polling collections - @DaleSeo PR #261
451 | 
452 | Right now, the MCP server restarts whenever there's a connectivity issue while polling collections from GraphOS. This causes the entire server to restart instead of handling the error gracefully.
453 | 
454 | ```
455 | Error: Failed to create operation: Error loading collection: error sending request for url (https://graphql.api.apollographql.com/api/graphql)
456 | Caused by:
457 |     Error loading collection: error sending request for url (https://graphql.api.apollographql.com/api/graphql)
458 | ```
459 | 
460 | This PR prevents server restarts by distinguishing between transient errors and permanent errors.
461 | 
462 | ## 🐛 Fixes
463 | 
464 | ### Keycloak OIDC discovery URL transformation - @DaleSeo PR #238
465 | 
466 | The MCP server currently replaces the entire path when building OIDC discovery URLs. This causes authentication failures for identity providers like Keycloak, which have path-based realms in the URL. This PR updates the URL transformation logic to preserve the existing path from the OAuth server URL.
467 | 
468 | ### fix: build error, let expressions unstable in while - @ThoreKoritzius #263
469 | 
470 | Fix unstable let expressions in while loop
471 | Replaced the unstable while let = expr syntax with a stable alternative, ensuring the code compiles on stable Rust without requiring nightly features.
472 | 
473 | ## 🛠 Maintenance
474 | 
475 | ### Address Security Vulnerabilities - @DaleSeo PR #264
476 | 
477 | This PR addresses the security vulnerabilities and dependency issues tracked in Dependency Dashboard #41 (https://osv.dev/vulnerability/RUSTSEC-2024-0388).
478 | 
479 | - Replaced the unmaintained `derivate` crate with the `educe` crate instead.
480 | - Updated the `tantivy` crate.
481 | 
482 | # [0.7.1] - 2025-08-13
483 | 
484 | ## 🚀 Features
485 | 
486 | ### feat: Pass `remote-mcp` mcp-session-id header along to GraphQL request - @damassi PR #236
487 | 
488 | This adds support for passing the `mcp-session-id` header through from `remote-mcp` via the MCP client config. This header [originates from the underlying `@modelcontextprotocol/sdk` library](https://github.com/modelcontextprotocol/typescript-sdk/blob/a1608a6513d18eb965266286904760f830de96fe/src/client/streamableHttp.ts#L182), invoked from `remote-mcp`.
489 | 
490 | With this change it is possible to correlate requests from MCP clients through to the final GraphQL server destination.
491 | 
492 | ## 🐛 Fixes
493 | 
494 | ### fix: Valid token fails validation with multiple audiences - @DaleSeo PR #244
495 | 
496 | Valid tokens are failing validation with the following error when the JWT tokens contain an audience claim as an array.
497 | 
498 | ```
499 | JSON error: invalid type: sequence, expected a string at line 1 column 97
500 | ```
501 | 
502 | According to [RFC 7519 Section 4.1.3](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3), the audience claim can be either a single string or an array of strings. However, our implementation assumes it will always be a string, which is causing this JSON parsing error.
503 | This fix updates the `Claims` struct to use `Vec<String>` instead of `String` for the `aud` field, along with a custom deserializer to handle both string and array formats.
504 | 
505 | ### fix: Add custom deserializer to handle APOLLO_UPLINK_ENDPOINTS environment variable parsing - @swcollard PR #220
506 | 
507 | The APOLLO_UPLINK_ENDPOINTS environment variables has historically been a comma separated list of URL strings.
508 | The move to yaml configuration allows us to more directly define the endpoints as a Vec.
509 | This fix introduces a custom deserializer for the `apollo_uplink_endpoints` config field that can handle both the environment variable comma separated string, and the yaml-based list.
510 | 
511 | # [0.7.0] - 2025-08-04
512 | 
513 | ## 🚀 Features
514 | 
515 | ### feat: add mcp auth - @nicholascioli PR #210
516 | 
517 | The MCP server can now be configured to act as an OAuth 2.1 resource server, following
518 | guidelines from the official MCP specification on Authorization / Authentication (see
519 | [the spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)).
520 | 
521 | To configure this new feature, a new `auth` section has been added to the SSE and
522 | Streamable HTTP transports. Below is an example configuration using Streamable HTTP:
523 | 
524 | ```yaml
525 | transport:
526 |   type: streamable_http
527 |   auth:
528 |     # List of upstream delegated OAuth servers
529 |     # Note: These need to support the OIDC metadata discovery endpoint
530 |     servers:
531 |       - https://auth.example.com
532 | 
533 |     # List of accepted audiences from upstream signed JWTs
534 |     # See: https://www.ory.sh/docs/hydra/guides/audiences
535 |     audiences:
536 |       - mcp.example.audience
537 | 
538 |     # The externally available URL pointing to this MCP server. Can be `localhost`
539 |     # when testing locally.
540 |     # Note: Subpaths must be preserved here as well. So append `/mcp` if using
541 |     # Streamable HTTP or `/sse` is using SSE.
542 |     resource: https://hosted.mcp.server/mcp
543 | 
544 |     # Optional link to more documentation relating to this MCP server.
545 |     resource_documentation: https://info.mcp.server
546 | 
547 |     # List of queryable OAuth scopes from the upstream OAuth servers
548 |     scopes:
549 |       - read
550 |       - mcp
551 |       - profile
552 | ```
553 | 
554 | ## 🐛 Fixes
555 | 
556 | ### Setting input_schema properties to empty when operation has no args ([Issue #136](https://github.com/apollographql/apollo-mcp-server/issues/136)) ([PR #212](https://github.com/apollographql/apollo-mcp-server/pull/212))
557 | 
558 | To support certain scenarios where a client fails on an omitted `properties` field within `input_schema`, setting the field to an empty map (`{}`) instead. While a missing `properties` field is allowed this will unblock
559 | certain users and allow them to use the MCP server.
560 | 
561 | # [0.6.1] - 2025-07-29
562 | 
563 | ## 🐛 Fixes
564 | 
565 | ### Handle headers from config file - @tylerscoville PR #213
566 | 
567 | Fix an issue where the server crashes when headers are set in the config file
568 | 
569 | ### Handle environment variables when no config file is provided - @DaleSeo PR #211
570 | 
571 | Fix an issue where the server fails with the message "Missing environment variable: APOLLO_GRAPH_REF," even when the variables are properly set.
572 | 
573 | ## 🚀 Features
574 | 
575 | ### Health Check Support - @DaleSeo PR #209
576 | 
577 | Health reporting functionality has been added to make the MCP server ready for production deployment with proper health monitoring and Kubernetes integration.
578 | 
579 | # [0.6.0] - 2025-07-14
580 | 
581 | ## ❗ BREAKING ❗
582 | 
583 | ### Replace CLI flags with a configuration file - @nicholascioli PR #162
584 | 
585 | All command line arguments are now removed and replaced with equivalent configuration
586 | options. The Apollo MCP server only accepts a single argument which is a path to a
587 | configuration file. An empty file may be passed, as all options have sane defaults
588 | that follow the previous argument defaults.
589 | 
590 | All options can be overridden by environment variables. They are of the following
591 | form:
592 | 
593 | - Prefixed by `APOLLO_MCP_`
594 | - Suffixed by the config equivalent path, with `__` marking nested options.
595 | 
596 | E.g. The environment variable to change the config option `introspection.execute.enabled`
597 | would be `APOLLO_MCP_INTROSPECTION__EXECUTE__ENABLED`.
598 | 
599 | Below is a valid configuration file with some options filled out:
600 | 
601 | ```yaml
602 | custom_scalars: /path/to/custom/scalars
603 | endpoint: http://127.0.0.1:4000
604 | graphos:
605 |   apollo_key: some.key
606 |   apollo_graph_ref: example@graph
607 | headers:
608 |   X-Some-Header: example-value
609 | introspection:
610 |   execute:
611 |     enabled: true
612 |   introspect:
613 |     enabled: false
614 | logging:
615 |   level: info
616 | operations:
617 |   source: local
618 |   paths:
619 |     - /path/to/operation.graphql
620 |     - /path/to/other/operation.graphql
621 | overrides:
622 |   disable_type_description: false
623 |   disable_schema_description: false
624 |   enable_explorer: false
625 |   mutation_mode: all
626 | schema:
627 |   source: local
628 |   path: /path/to/schema.graphql
629 | transport:
630 |   type: streamable_http
631 |   address: 127.0.0.1
632 |   port: 5000
633 | ```
634 | 
635 | ## 🚀 Features
636 | 
637 | ### Validate tool for verifying graphql queries before executing them - @swcollard PR #203
638 | 
639 | The introspection options in the mcp server provide introspect, execute, and search tools. The LLM often tries to validate its queries by just executing them. This may not be desired (there might be side effects, for example). This feature adds a `validate` tool so the LLM can validate the operation without actually hitting the GraphQL endpoint. It first validates the syntax of the operation, and then checks it against the introspected schema for validation.
640 | 
641 | ### Minify introspect return value - @pubmodmatt PR #178
642 | 
643 | The `introspect` and `search` tools now have an option to minify results. Minified GraphQL SDL takes up less space in the context window.
644 | 
645 | ### Add search tool - @pubmodmatt PR #171
646 | 
647 | A new experimental `search` tool has been added that allows the AI model to specify a set of terms to search for in the GraphQL schema. The top types matching that search are returned, as well as enough information to enable creation of GraphQL operations involving those types.
648 | 
649 | # [0.5.2] - 2025-07-10
650 | 
651 | ## 🐛 Fixes
652 | 
653 | ### Fix ServerInfo - @pubmodmatt PR #183
654 | 
655 | The server will now report the correct server name and version to clients, rather than the Rust MCP SDK name and version.
656 | 
657 | # [0.5.1] - 2025-07-08
658 | 
659 | ## 🐛 Fixes
660 | 
661 | ### Fix an issue with rmcp 0.2.x upgrade - @pubmodmatt PR #181
662 | 
663 | Fix an issue where the server was unresponsive to external events such as changes to operation collections.
664 | 
665 | # [0.5.0] - 2025-07-08
666 | 
667 | ## ❗ BREAKING ❗
668 | 
669 | ### Deprecate -u,--uplink argument and use default collection - @Jephuff PR #154
670 | 
671 | `--uplink` and `-u` are deprecated and will act as an alias for `--uplink-manifest`. If a schema isn't provided, it will get fetched from uplink by default, and `--uplink-manifest` can be used to fetch the persisted queries from uplink.
672 | The server will now default to the default MCP tools from operation collections.
673 | 
674 | ## 🚀 Features
675 | 
676 | ### Add --version argument - @Jephuff PR #154
677 | 
678 | `apollo-mcp-server --version` will print the version of apollo-mcp-server currently installed
679 | 
680 | ### Support operation variable comments as description overrides - @alocay PR #164
681 | 
682 | Operation comments for variables will now act as overrides for variable descriptions
683 | 
684 | ### Include operation name with GraphQL requests - @DaleSeo PR #166
685 | 
686 | Include the operation name with GraphQL requests if it's available.
687 | 
688 | ```diff
689 | {
690 |    "query":"query GetAlerts(: String!) { alerts(state: ) { severity description instruction } }",
691 |    "variables":{
692 |       "state":"CO"
693 |    },
694 |    "extensions":{
695 |       "clientLibrary":{
696 |          "name":"mcp",
697 |          "version": ...
698 |       }
699 |    },
700 | +  "operationName":"GetAlerts"
701 | }
702 | ```
703 | 
704 | ## 🐛 Fixes
705 | 
706 | ### The execute tool handles invalid operation types - @DaleSeo PR #170
707 | 
708 | The execute tool returns an invalid parameters error when the operation type does not match the mutation mode.
709 | 
710 | ### Skip unnamed operations and log a warning instead of crashing - @DaleSeo PR #173
711 | 
712 | Unnamed operations are now skipped with a warning instead of causing the server to crash
713 | 
714 | ### Support retaining argument descriptions from schema for variables - @alocay PR #147
715 | 
716 | Use descriptions for arguments from schema when building descriptions for operation variables.
717 | 
718 | ### Invalid operation should not crash the MCP Server - @DaleSeo PR #176
719 | 
720 | Gracefully handle and skip invalid GraphQL operations to prevent MCP server crashes during startup or runtime.
721 | 
722 | # [0.4.2] - 2025-06-24
723 | 
724 | ## 🚀 Features
725 | 
726 | ### Pass in --collection default to use default collection - @Jephuff PR #151
727 | 
728 | --collection default will use the configured default collection on the graph variant specified by the --apollo-graph-ref arg
729 | 
730 | # [0.4.1] - 2025-06-20
731 | 
732 | ## 🐛 Fixes
733 | 
734 | ### Fix tool update on every poll - @Jephuff PR #146
735 | 
736 | Only update the tool list if an operation was removed, changed, or added.
737 | 
738 | # [0.4.0] - 2025-06-17
739 | 
740 | ## 🚀 Features
741 | 
742 | ### Add `--collection <COLLECTION_ID>` as another option for operation source - @Jephuff PR #118
743 | 
744 | Use operation collections as the source of operations for your MCP server. The server will watch for changes and automatically update when you change your operation collection.
745 | 
746 | ### Allow overriding registry endpoints - @Jephuff PR #134
747 | 
748 | Set APOLLO_UPLINK_ENDPOINTS and APOLLO_REGISTRY_URL to override the endpoints for fetching schemas and operations
749 | 
750 | ### Add client metadata to GraphQL requests - @pubmodmatt PR #137
751 | 
752 | The MCP Server will now identify itself to Apollo Router through the `ApolloClientMetadata` extension. This allows traffic from MCP to be identified in the router, for example through telemetry.
753 | 
754 | ### Update license to MIT - @kbychu PR #122
755 | 
756 | The Apollo MCP Server is now licensed under MIT instead of ELv2
757 | 
758 | ## 🐛 Fixes
759 | 
760 | ### Fix GetAstronautsCurrentlyInSpace query - @pubmodmatt PR #114
761 | 
762 | The `GetAstronautsCurrentlyInSpace` in the Quickstart documentation was not working.
763 | 
764 | ### Change explorer tool to return URL - @pubmodmatt PR #123
765 | 
766 | The explorer tool previously opened the GraphQL query directly in the user's browser. Although convenient, this would only work if the MCP Server was hosted on the end user's machine, not remotely. It will now return the URL instead.
767 | 
768 | ### Fix bug in operation directory watching - @pubmodmatt PR #135
769 | 
770 | Operation directory watching would not trigger an update of operations in some cases.
771 | 
772 | ### fix: handle headers with colons in value - @DaleSeo PR #128
773 | 
774 | The MCP server won't crash when a header's value contains colons.
775 | 
776 | ## 🛠 Maintenance
777 | 
778 | ### Automate changesets and changelog - @pubmodmatt PR #107
779 | 
780 | Contributors can now generate a changeset file automatically with:
781 | 
782 | ```console
783 | cargo xtask changeset create
784 | ```
785 | 
786 | This will generate a file in the `.changesets` directory, which can be added to the pull request.
787 | 
788 | ## [0.3.0] - 2025-05-29
789 | 
790 | ### 🚀 Features
791 | 
792 | - Implement the Streamable HTTP transport. Enable with `--http-port` and/or `--http-address`. (#98)
793 | - Include both the type description and field description in input schema (#100)
794 | - Hide String, ID, Int, Float, and Boolean descriptions in input schema (#100)
795 | - Set the `readOnlyHint` tool annotation for tools based on GraphQL query operations (#103)
796 | 
797 | ### 🐛 Fixes
798 | 
799 | - Fix error with recursive input types (#100)
800 | 
801 | ## [0.2.1] - 2025-05-27
802 | 
803 | ### 🐛 Fixes
804 | 
805 | - Reduce the log level of many messages emitted by the server so INFO is less verbose, and add a `--log` option to specify the log level used by the MCP Server (default is INFO) (#82)
806 | - Ignore mutations and subscriptions rather than erroring out (#91)
807 | - Silence \_\_typename used in operations errors (#79)
808 | - Fix issues with the `introspect` tool. (#83)
809 |   - The tool was not working when there were top-level subscription in the schema
810 |   - Argument types were not being resolved correctly
811 | - Improvements to operation loading (#80)
812 |   - When specifying multiple operation paths, all paths were reloaded when any one changed
813 |   - Many redundant events were sent on startup, causing verbose logging about loaded operations
814 |   - Better error handling for missing, invalid, or empty operation files
815 | - The `execute` tool did not handle variables correctly (#77 and #93)
816 | - Cycles in schema type definitions would lead to stack overflow (#74)
817 | 
818 | ## [0.2.0] - 2025-05-21
819 | 
820 | ### 🚀 Features
821 | 
822 | - The `--operations` argument now supports hot reloading and directory paths. If a directory is specified, all .graphql files in the directory will be loaded as operations. The running server will update when files are added to or removed from the directory. (#69)
823 | - Add an optional `--sse-address` argument to set the bind address of the MCP server. Defaults to 127.0.0.1. (#63)
824 | 
825 | ### 🐛 Fixes
826 | 
827 | - Fixed PowerShell script (#55)
828 | - Log to stdout, not stderr (#59)
829 | - The `--directory` argument is now optional. When using the stdio transport, it is recommended to either set this option or use absolute paths for other arguments. (#64)
830 | 
831 | ### 📚 Documentation
832 | 
833 | - Fix and simplify the example `rover dev --mcp` commands
834 | 
835 | ## [0.1.0] - 2025-05-15
836 | 
837 | ### 🚀 Features
838 | 
839 | - Initial release of the Apollo MCP Server
840 | 
```

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

```rust
   1 | //! Tree shaking for GraphQL schemas
   2 | 
   3 | use apollo_compiler::ast::{
   4 |     Argument, Definition, DirectiveDefinition, DirectiveList, Document, EnumTypeDefinition, Field,
   5 |     FragmentDefinition, InputObjectTypeDefinition, InterfaceTypeDefinition, ObjectTypeDefinition,
   6 |     OperationDefinition, OperationType, ScalarTypeDefinition, SchemaDefinition, Selection,
   7 |     UnionTypeDefinition,
   8 | };
   9 | use apollo_compiler::schema::ExtendedType;
  10 | use apollo_compiler::schema::InputValueDefinition;
  11 | use apollo_compiler::validation::WithErrors;
  12 | use apollo_compiler::{Name, Node, Schema};
  13 | use std::collections::HashMap;
  14 | use tracing::debug;
  15 | 
  16 | struct RootOperationNames {
  17 |     query: String,
  18 |     mutation: String,
  19 |     subscription: String,
  20 | }
  21 | 
  22 | impl RootOperationNames {
  23 |     fn operation_name(
  24 |         operation_type: OperationType,
  25 |         default_name: &str,
  26 |         schema: &Schema,
  27 |     ) -> String {
  28 |         schema
  29 |             .root_operation(operation_type)
  30 |             .map(|r| r.to_string())
  31 |             .unwrap_or(default_name.to_string())
  32 |     }
  33 | 
  34 |     fn new(schema: &Schema) -> Self {
  35 |         Self {
  36 |             query: Self::operation_name(OperationType::Query, "query", schema),
  37 |             mutation: Self::operation_name(OperationType::Mutation, "mutation", schema),
  38 |             subscription: Self::operation_name(OperationType::Subscription, "subscription", schema),
  39 |         }
  40 |     }
  41 | 
  42 |     fn name_for_operation_type(&self, operation_type: OperationType) -> &str {
  43 |         match operation_type {
  44 |             OperationType::Query => &self.query,
  45 |             OperationType::Mutation => &self.mutation,
  46 |             OperationType::Subscription => &self.subscription,
  47 |         }
  48 |     }
  49 | }
  50 | 
  51 | /// Limits the depth of the schema tree that is retained.
  52 | #[derive(Debug, Clone, Copy)]
  53 | pub enum DepthLimit {
  54 |     Unlimited,
  55 |     Limited(usize),
  56 | }
  57 | 
  58 | impl DepthLimit {
  59 |     /// Returns true if the depth limit has been reached.
  60 |     pub fn reached(&self) -> bool {
  61 |         match self {
  62 |             DepthLimit::Unlimited => false,
  63 |             DepthLimit::Limited(depth) => *depth == 0,
  64 |         }
  65 |     }
  66 | 
  67 |     /// Decrements the depth limit. This should be called when descending a level in the schema type tree.
  68 |     pub fn decrement(self) -> Self {
  69 |         match self {
  70 |             DepthLimit::Unlimited => self,
  71 |             DepthLimit::Limited(depth) => DepthLimit::Limited(depth - 1),
  72 |         }
  73 |     }
  74 | }
  75 | 
  76 | /// Tree shaker for GraphQL schemas
  77 | pub struct SchemaTreeShaker<'schema> {
  78 |     schema: &'schema Schema,
  79 |     named_type_nodes: HashMap<String, TreeTypeNode>,
  80 |     directive_nodes: HashMap<String, TreeDirectiveNode<'schema>>,
  81 |     operation_types: Vec<OperationType>,
  82 |     operation_type_names: RootOperationNames,
  83 |     named_fragments: HashMap<String, Node<FragmentDefinition>>,
  84 |     arguments_descriptions: HashMap<String, Vec<String>>,
  85 | }
  86 | 
  87 | struct TreeTypeNode {
  88 |     retain: bool,
  89 |     filtered_field: Option<Vec<String>>,
  90 | }
  91 | 
  92 | struct TreeDirectiveNode<'schema> {
  93 |     node: &'schema DirectiveDefinition,
  94 |     retain: bool,
  95 | }
  96 | 
  97 | impl<'schema> SchemaTreeShaker<'schema> {
  98 |     pub(crate) fn argument_descriptions(&self) -> &HashMap<String, Vec<String>> {
  99 |         &self.arguments_descriptions
 100 |     }
 101 | 
 102 |     pub fn new(schema: &'schema Schema) -> Self {
 103 |         let mut named_type_nodes: HashMap<String, TreeTypeNode> = HashMap::default();
 104 |         let mut directive_nodes: HashMap<String, TreeDirectiveNode> = HashMap::default();
 105 | 
 106 |         schema.types.iter().for_each(|(_name, extended_type)| {
 107 |             let key = extended_type.name().as_str();
 108 |             if named_type_nodes.contains_key(key) {
 109 |                 tracing::error!("type {} already exists", key);
 110 |             }
 111 |             named_type_nodes.insert(
 112 |                 key.to_string(),
 113 |                 TreeTypeNode {
 114 |                     filtered_field: None,
 115 |                     retain: false,
 116 |                 },
 117 |             );
 118 |         });
 119 | 
 120 |         schema
 121 |             .directive_definitions
 122 |             .iter()
 123 |             .for_each(|(name, directive_def)| {
 124 |                 let key = name.as_str();
 125 |                 if directive_nodes.contains_key(key) {
 126 |                     tracing::error!("directive {} already exists", key);
 127 |                 }
 128 |                 directive_nodes.insert(
 129 |                     key.to_string(),
 130 |                     TreeDirectiveNode {
 131 |                         node: directive_def,
 132 |                         retain: false,
 133 |                     },
 134 |                 );
 135 |             });
 136 | 
 137 |         Self {
 138 |             schema,
 139 |             named_type_nodes,
 140 |             directive_nodes,
 141 |             operation_types: Vec::default(),
 142 |             named_fragments: HashMap::default(),
 143 |             operation_type_names: RootOperationNames::new(schema),
 144 |             arguments_descriptions: HashMap::default(),
 145 |         }
 146 |     }
 147 | 
 148 |     pub fn retain_operation_type(
 149 |         &mut self,
 150 |         operation_type: OperationType,
 151 |         selection_set: Option<&Vec<Selection>>,
 152 |         depth_limit: DepthLimit,
 153 |     ) {
 154 |         self.operation_types.push(operation_type);
 155 |         let operation_type_name = self
 156 |             .operation_type_names
 157 |             .name_for_operation_type(operation_type);
 158 | 
 159 |         if let Some(operation_type_extended_type) = self.schema.types.get(operation_type_name) {
 160 |             retain_type(
 161 |                 self,
 162 |                 operation_type_extended_type,
 163 |                 selection_set,
 164 |                 depth_limit,
 165 |             );
 166 |         } else {
 167 |             tracing::error!("root operation type {} not found in schema", operation_type);
 168 |         }
 169 |     }
 170 | 
 171 |     /// Retain a specific type, and recursively every type it references, up to a given depth.
 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);
 179 |     }
 180 | 
 181 |     pub fn retain_operation(
 182 |         &mut self,
 183 |         operation: &OperationDefinition,
 184 |         document: &Document,
 185 |         depth_limit: DepthLimit,
 186 |     ) {
 187 |         self.named_fragments = document
 188 |             .definitions
 189 |             .iter()
 190 |             .filter_map(|def| match def {
 191 |                 Definition::FragmentDefinition(fragment_def) => {
 192 |                     Some((fragment_def.name.to_string(), fragment_def.clone()))
 193 |                 }
 194 |                 _ => None,
 195 |             })
 196 |             .collect();
 197 |         self.retain_operation_type(
 198 |             operation.operation_type,
 199 |             Some(&operation.selection_set),
 200 |             depth_limit,
 201 |         )
 202 |     }
 203 | 
 204 |     /// Return the set of types retained after tree shaking.
 205 |     pub fn shaken(&mut self) -> Result<Schema, Box<WithErrors<Schema>>> {
 206 |         let root_operations = self
 207 |             .operation_types
 208 |             .iter()
 209 |             .filter_map(|operation_type| {
 210 |                 self.schema
 211 |                     .root_operation(*operation_type)
 212 |                     .cloned()
 213 |                     .map(|operation_name| Node::new((*operation_type, operation_name)))
 214 |             })
 215 |             .collect();
 216 | 
 217 |         let schema_definition =
 218 |             Definition::SchemaDefinition(apollo_compiler::Node::new(SchemaDefinition {
 219 |                 root_operations,
 220 |                 description: self.schema.schema_definition.description.clone(),
 221 |                 directives: DirectiveList(
 222 |                     self.schema
 223 |                         .schema_definition
 224 |                         .directives
 225 |                         .0
 226 |                         .iter()
 227 |                         .map(|directive| directive.node.clone())
 228 |                         .collect(),
 229 |                 ),
 230 |             }));
 231 | 
 232 |         let directive_definitions = self
 233 |             .schema
 234 |             .directive_definitions
 235 |             .iter()
 236 |             .filter_map(|(directive_name, directive_def)| {
 237 |                 self.directive_nodes
 238 |                     .get(directive_name.as_str())
 239 |                     .and_then(|n| {
 240 |                         (!directive_def.is_built_in() && n.retain)
 241 |                             .then_some(Definition::DirectiveDefinition(directive_def.clone()))
 242 |                     })
 243 |             })
 244 |             .collect();
 245 | 
 246 |         let type_definitions: Vec<_> = self
 247 |             .schema
 248 |             .types
 249 |             .iter()
 250 |             .filter_map(|(_type_name, extended_type)| {
 251 |                 if extended_type.is_built_in() {
 252 |                     None
 253 |                 } else {
 254 |                     match extended_type {
 255 |                         ExtendedType::Object(object_def) => self
 256 |                             .named_type_nodes
 257 |                             .get(object_def.name.as_str())
 258 |                             .and_then(|tree_node| {
 259 |                                 if tree_node.retain {
 260 |                                     Some(Definition::ObjectTypeDefinition(Node::new(
 261 |                                         ObjectTypeDefinition {
 262 |                                             description: object_def.description.clone(),
 263 |                                             directives: DirectiveList(
 264 |                                                 object_def
 265 |                                                     .directives
 266 |                                                     .0
 267 |                                                     .iter()
 268 |                                                     .map(|directive| directive.node.clone())
 269 |                                                     .collect(),
 270 |                                             ),
 271 |                                             name: object_def.name.clone(),
 272 |                                             implements_interfaces: object_def
 273 |                                                 .implements_interfaces
 274 |                                                 .iter()
 275 |                                                 .map(|implemented_interface| {
 276 |                                                     implemented_interface.name.clone()
 277 |                                                 })
 278 |                                                 .collect(),
 279 |                                             fields: object_def
 280 |                                                 .fields
 281 |                                                 .clone()
 282 |                                                 .into_iter()
 283 |                                                 .filter_map(|(field_name, field)| {
 284 |                                                     if let Some(filtered_fields) =
 285 |                                                         &tree_node.filtered_field
 286 |                                                     {
 287 |                                                         filtered_fields
 288 |                                                             .contains(&field_name.to_string())
 289 |                                                             .then_some(field.node)
 290 |                                                     } else {
 291 |                                                         Some(field.node)
 292 |                                                     }
 293 |                                                 })
 294 |                                                 .collect(),
 295 |                                         },
 296 |                                     )))
 297 |                                 } else if self.schema.root_operation(OperationType::Query).is_some()
 298 |                                 {
 299 |                                     None
 300 |                                 } else {
 301 |                                     tracing::error!("object type {} not found", object_def.name);
 302 |                                     None
 303 |                                 }
 304 |                             }),
 305 |                         ExtendedType::InputObject(input_def) => self
 306 |                             .named_type_nodes
 307 |                             .get(input_def.name.as_str())
 308 |                             .and_then(|tree_node| {
 309 |                                 if tree_node.retain {
 310 |                                     Some(Definition::InputObjectTypeDefinition(Node::new(
 311 |                                         InputObjectTypeDefinition {
 312 |                                             description: input_def.description.clone(),
 313 |                                             directives: DirectiveList(
 314 |                                                 input_def
 315 |                                                     .directives
 316 |                                                     .0
 317 |                                                     .iter()
 318 |                                                     .map(|directive| directive.node.clone())
 319 |                                                     .collect(),
 320 |                                             ),
 321 |                                             name: input_def.name.clone(),
 322 |                                             fields: input_def
 323 |                                                 .fields
 324 |                                                 .clone()
 325 |                                                 .into_iter()
 326 |                                                 .filter_map(|(field_name, field)| {
 327 |                                                     if let Some(filtered_fields) =
 328 |                                                         &tree_node.filtered_field
 329 |                                                     {
 330 |                                                         filtered_fields
 331 |                                                             .contains(&field_name.to_string())
 332 |                                                             .then_some(field.node)
 333 |                                                     } else {
 334 |                                                         Some(field.node)
 335 |                                                     }
 336 |                                                 })
 337 |                                                 .collect(),
 338 |                                         },
 339 |                                     )))
 340 |                                 } else {
 341 |                                     None
 342 |                                 }
 343 |                             }),
 344 |                         ExtendedType::Interface(interface_def) => self
 345 |                             .named_type_nodes
 346 |                             .get(interface_def.name.as_str())
 347 |                             .and_then(|tree_node| {
 348 |                                 if tree_node.retain {
 349 |                                     Some(Definition::InterfaceTypeDefinition(Node::new(
 350 |                                         InterfaceTypeDefinition {
 351 |                                             description: interface_def.description.clone(),
 352 |                                             directives: DirectiveList(
 353 |                                                 interface_def
 354 |                                                     .directives
 355 |                                                     .0
 356 |                                                     .iter()
 357 |                                                     .map(|directive| directive.node.clone())
 358 |                                                     .collect(),
 359 |                                             ),
 360 |                                             name: interface_def.name.clone(),
 361 |                                             implements_interfaces: interface_def
 362 |                                                 .implements_interfaces
 363 |                                                 .iter()
 364 |                                                 .map(|implemented_interface| {
 365 |                                                     implemented_interface.name.clone()
 366 |                                                 })
 367 |                                                 .collect(),
 368 |                                             fields: interface_def
 369 |                                                 .fields
 370 |                                                 .clone()
 371 |                                                 .into_iter()
 372 |                                                 .filter_map(|(field_name, field)| {
 373 |                                                     if let Some(filtered_fields) =
 374 |                                                         &tree_node.filtered_field
 375 |                                                     {
 376 |                                                         filtered_fields
 377 |                                                             .contains(&field_name.to_string())
 378 |                                                             .then_some(field.node)
 379 |                                                     } else {
 380 |                                                         Some(field.node)
 381 |                                                     }
 382 |                                                 })
 383 |                                                 .collect(),
 384 |                                         },
 385 |                                     )))
 386 |                                 } else {
 387 |                                     None
 388 |                                 }
 389 |                             }),
 390 |                         ExtendedType::Union(union_def) => self
 391 |                             .named_type_nodes
 392 |                             .get(union_def.name.as_str())
 393 |                             .is_some_and(|n| n.retain)
 394 |                             .then(|| {
 395 |                                 Definition::UnionTypeDefinition(Node::new(UnionTypeDefinition {
 396 |                                     description: union_def.description.clone(),
 397 |                                     directives: DirectiveList(
 398 |                                         union_def
 399 |                                             .directives
 400 |                                             .0
 401 |                                             .iter()
 402 |                                             .map(|directive| directive.node.clone())
 403 |                                             .collect(),
 404 |                                     ),
 405 |                                     name: union_def.name.clone(),
 406 |                                     members: union_def
 407 |                                         .members
 408 |                                         .clone()
 409 |                                         .into_iter()
 410 |                                         .filter_map(|member| {
 411 |                                             if let Some(member_tree_node) =
 412 |                                                 self.named_type_nodes.get(member.as_str())
 413 |                                             {
 414 |                                                 member_tree_node.retain.then_some(member.name)
 415 |                                             } else {
 416 |                                                 tracing::error!(
 417 |                                                     "union member {} not found",
 418 |                                                     member
 419 |                                                 );
 420 |                                                 None
 421 |                                             }
 422 |                                         })
 423 |                                         .collect(),
 424 |                                 }))
 425 |                             }),
 426 |                         ExtendedType::Enum(enum_def) => self
 427 |                             .named_type_nodes
 428 |                             .get(enum_def.name.as_str())
 429 |                             .and_then(|tree_node| {
 430 |                                 if tree_node.retain {
 431 |                                     Some(Definition::EnumTypeDefinition(Node::new(
 432 |                                         EnumTypeDefinition {
 433 |                                             description: enum_def.description.clone(),
 434 |                                             directives: DirectiveList(
 435 |                                                 enum_def
 436 |                                                     .directives
 437 |                                                     .0
 438 |                                                     .iter()
 439 |                                                     .map(|directive| directive.node.clone())
 440 |                                                     .collect(),
 441 |                                             ),
 442 |                                             name: enum_def.name.clone(),
 443 |                                             values: enum_def
 444 |                                                 .values
 445 |                                                 .iter()
 446 |                                                 .map(|(_enum_value_name, enum_value)| {
 447 |                                                     enum_value.node.clone()
 448 |                                                 })
 449 |                                                 .collect(),
 450 |                                         },
 451 |                                     )))
 452 |                                 } else {
 453 |                                     None
 454 |                                 }
 455 |                             }),
 456 |                         ExtendedType::Scalar(scalar_def) => self
 457 |                             .named_type_nodes
 458 |                             .get(scalar_def.name.as_str())
 459 |                             .and_then(|tree_node| {
 460 |                                 if tree_node.retain {
 461 |                                     Some(Definition::ScalarTypeDefinition(Node::new(
 462 |                                         ScalarTypeDefinition {
 463 |                                             description: scalar_def.description.clone(),
 464 |                                             directives: DirectiveList(
 465 |                                                 scalar_def
 466 |                                                     .directives
 467 |                                                     .0
 468 |                                                     .iter()
 469 |                                                     .map(|directive| directive.node.clone())
 470 |                                                     .collect(),
 471 |                                             ),
 472 |                                             name: scalar_def.name.clone(),
 473 |                                         },
 474 |                                     )))
 475 |                                 } else {
 476 |                                     None
 477 |                                 }
 478 |                             }),
 479 |                     }
 480 |                 }
 481 |             })
 482 |             .collect();
 483 | 
 484 |         debug!("Tree shaking resulted in {} types", type_definitions.len());
 485 | 
 486 |         let mut document = Document::new();
 487 |         document.definitions = [
 488 |             // // TODO: don't push if theres no data
 489 |             vec![schema_definition],
 490 |             directive_definitions,
 491 |             type_definitions,
 492 |         ]
 493 |         .concat();
 494 | 
 495 |         document.to_schema().map_err(Box::new)
 496 |     }
 497 | }
 498 | 
 499 | fn selection_set_to_fields(
 500 |     selection_set: &Selection,
 501 |     named_fragments: &HashMap<String, Node<FragmentDefinition>>,
 502 | ) -> Vec<Node<Field>> {
 503 |     match selection_set {
 504 |         Selection::Field(field) => {
 505 |             if field.name == "__typename" {
 506 |                 vec![]
 507 |             } else {
 508 |                 vec![field.clone()]
 509 |             }
 510 |         }
 511 |         Selection::FragmentSpread(fragment) => named_fragments
 512 |             .get(fragment.fragment_name.as_str())
 513 |             .map(|f| {
 514 |                 f.selection_set
 515 |                     .iter()
 516 |                     .flat_map(|s| selection_set_to_fields(s, named_fragments))
 517 |                     .collect()
 518 |             })
 519 |             .unwrap_or_default(),
 520 |         Selection::InlineFragment(fragment) => fragment
 521 |             .selection_set
 522 |             .iter()
 523 |             .flat_map(|s| selection_set_to_fields(s, named_fragments))
 524 |             .collect(),
 525 |     }
 526 | }
 527 | 
 528 | fn retain_argument_descriptions(
 529 |     tree_shaker: &mut SchemaTreeShaker,
 530 |     arg: &Node<InputValueDefinition>,
 531 |     operation_arguments: &HashMap<&str, &Name>,
 532 | ) {
 533 |     let operation_argument_name = operation_arguments.get(arg.name.as_str());
 534 | 
 535 |     if let Some(op_arg_name) = operation_argument_name
 536 |         && let Some(description) = arg.description.as_deref()
 537 |         && !description.trim().is_empty()
 538 |     {
 539 |         let descriptions = tree_shaker
 540 |             .arguments_descriptions
 541 |             .entry(op_arg_name.to_string())
 542 |             .or_default();
 543 |         descriptions.push(description.trim().to_string())
 544 |     }
 545 | }
 546 | 
 547 | fn build_argument_name_to_value_map(arguments: &[Node<Argument>]) -> HashMap<&str, &Name> {
 548 |     arguments
 549 |         .iter()
 550 |         .filter_map(|a| a.value.as_variable().map(|v| (a.name.as_str(), v)))
 551 |         .collect::<HashMap<_, _>>()
 552 | }
 553 | 
 554 | fn retain_type(
 555 |     tree_shaker: &mut SchemaTreeShaker,
 556 |     extended_type: &ExtendedType,
 557 |     selection_set: Option<&Vec<Selection>>,
 558 |     depth_limit: DepthLimit,
 559 | ) {
 560 |     // Check if we've exceeded the depth limit
 561 |     if depth_limit.reached() {
 562 |         return;
 563 |     }
 564 | 
 565 |     let type_name = extended_type.name().as_str();
 566 |     let selected_fields = if let Some(selection_set) = selection_set {
 567 |         let selected_fields = selection_set
 568 |             .iter()
 569 |             .flat_map(|s| selection_set_to_fields(s, &tree_shaker.named_fragments))
 570 |             .collect::<Vec<_>>();
 571 | 
 572 |         Some(selected_fields)
 573 |     } else {
 574 |         None
 575 |     };
 576 | 
 577 |     if let Some(tree_node) = tree_shaker.named_type_nodes.get_mut(type_name) {
 578 |         // If we have already visited this node, early return to avoid infinite recursion.
 579 |         // depth_limit and selection_set both have inherent exit cases and may add more types with multiple passes, so never early return for them.
 580 |         if tree_node.retain
 581 |             && selection_set.is_none()
 582 |             && matches!(depth_limit, DepthLimit::Unlimited)
 583 |         {
 584 |             return;
 585 |         }
 586 | 
 587 |         tree_node.retain = true;
 588 |         if let Some(selected_fields) = selected_fields.as_ref() {
 589 |             let additional_fields = selected_fields
 590 |                 .iter()
 591 |                 .map(|f| f.name.to_string())
 592 |                 .collect::<Vec<_>>();
 593 | 
 594 |             tree_node.filtered_field = Some(
 595 |                 [
 596 |                     tree_node.filtered_field.clone().unwrap_or_default(),
 597 |                     additional_fields,
 598 |                 ]
 599 |                 .concat(),
 600 |             );
 601 |         }
 602 |     }
 603 | 
 604 |     extended_type
 605 |         .directives()
 606 |         .iter()
 607 |         .for_each(|t| retain_directive(tree_shaker, t.name.as_str(), depth_limit));
 608 | 
 609 |     match extended_type {
 610 |         ExtendedType::Object(def) => {
 611 |             selected_fields
 612 |                 .as_ref()
 613 |                 .map(|fields| {
 614 |                     fields
 615 |                         .iter()
 616 |                         .map(|field| {
 617 |                             (
 618 |                                 field.name.as_str(),
 619 |                                 def.fields.get(field.name.as_str()),
 620 |                                 Some(&field.directives),
 621 |                                 Some(&field.selection_set),
 622 |                                 build_argument_name_to_value_map(&field.arguments),
 623 |                             )
 624 |                         })
 625 |                         .collect::<Vec<_>>()
 626 |                 })
 627 |                 .unwrap_or(
 628 |                     def.fields
 629 |                         .iter()
 630 |                         .map(|(name, field_definition)| {
 631 |                             (
 632 |                                 name.as_str(),
 633 |                                 Some(field_definition),
 634 |                                 None,
 635 |                                 None,
 636 |                                 HashMap::default(),
 637 |                             )
 638 |                         })
 639 |                         .collect::<Vec<_>>(),
 640 |                 )
 641 |                 .into_iter()
 642 |                 .for_each(
 643 |                     |(
 644 |                         field_name,
 645 |                         field_definition,
 646 |                         field_selection_directives,
 647 |                         field_selection_set,
 648 |                         field_arguments,
 649 |                     )| {
 650 |                         if let Some(field_type) = field_definition {
 651 |                             let field_type_name = field_type.ty.inner_named_type();
 652 |                             if let Some(field_type_def) =
 653 |                                 tree_shaker.schema.types.get(field_type_name)
 654 |                             {
 655 |                                 retain_type(
 656 |                                     tree_shaker,
 657 |                                     field_type_def,
 658 |                                     field_selection_set,
 659 |                                     depth_limit.decrement(),
 660 |                                 );
 661 |                             } else {
 662 |                                 tracing::error!("field type {} not found", field_type_name);
 663 |                             }
 664 | 
 665 |                             field_type.arguments.iter().for_each(|arg| {
 666 |                                 retain_argument_descriptions(tree_shaker, arg, &field_arguments);
 667 | 
 668 |                                 let arg_type_name = arg.ty.inner_named_type();
 669 |                                 if let Some(arg_type) = tree_shaker.schema.types.get(arg_type_name)
 670 |                                 {
 671 |                                     retain_type(
 672 |                                         tree_shaker,
 673 |                                         arg_type,
 674 |                                         None,
 675 |                                         depth_limit.decrement(),
 676 |                                     );
 677 |                                 } else {
 678 |                                     tracing::error!(
 679 |                                         "field argument type {} not found",
 680 |                                         arg_type_name
 681 |                                     );
 682 |                                 }
 683 |                             });
 684 |                         } else {
 685 |                             tracing::error!("field {} not found", field_name);
 686 |                         }
 687 | 
 688 |                         if let Some(field_definition_directives) =
 689 |                             field_definition.map(|f| f.directives.clone())
 690 |                         {
 691 |                             field_definition_directives.iter().for_each(|directive| {
 692 |                                 retain_directive(tree_shaker, directive.name.as_str(), depth_limit);
 693 |                             })
 694 |                         }
 695 |                         if let Some(field_selection_directives) = field_selection_directives {
 696 |                             field_selection_directives.iter().for_each(|directive| {
 697 |                                 if let Some(directive_definition) = tree_shaker
 698 |                                     .schema
 699 |                                     .directive_definitions
 700 |                                     .get(&directive.name)
 701 |                                 {
 702 |                                     let directive_args_map =
 703 |                                         build_argument_name_to_value_map(&directive.arguments);
 704 |                                     directive_definition.arguments.iter().for_each(|arg| {
 705 |                                         retain_argument_descriptions(
 706 |                                             tree_shaker,
 707 |                                             arg,
 708 |                                             &directive_args_map,
 709 |                                         );
 710 |                                     });
 711 |                                 }
 712 | 
 713 |                                 retain_directive(tree_shaker, directive.name.as_str(), depth_limit);
 714 |                             })
 715 |                         }
 716 |                     },
 717 |                 );
 718 |         }
 719 |         ExtendedType::Interface(def) => {
 720 |             selected_fields
 721 |                 .as_ref()
 722 |                 .map(|fields| {
 723 |                     fields
 724 |                         .iter()
 725 |                         .map(|field| {
 726 |                             (
 727 |                                 field.name.as_str(),
 728 |                                 def.fields.get(field.name.as_str()),
 729 |                                 Some(&field.directives),
 730 |                                 Some(&field.selection_set),
 731 |                             )
 732 |                         })
 733 |                         .collect::<Vec<_>>()
 734 |                 })
 735 |                 .unwrap_or(
 736 |                     def.fields
 737 |                         .iter()
 738 |                         .map(|(name, field_definition)| {
 739 |                             (name.as_str(), Some(field_definition), None, None)
 740 |                         })
 741 |                         .collect::<Vec<_>>(),
 742 |                 )
 743 |                 .into_iter()
 744 |                 .for_each(
 745 |                     |(
 746 |                         field_name,
 747 |                         field_definition,
 748 |                         field_selection_directives,
 749 |                         field_selection_set,
 750 |                     )| {
 751 |                         if let Some(field_type) = field_definition {
 752 |                             let field_type_name = field_type.ty.inner_named_type();
 753 |                             if let Some(field_type_def) =
 754 |                                 tree_shaker.schema.types.get(field_type_name)
 755 |                             {
 756 |                                 retain_type(
 757 |                                     tree_shaker,
 758 |                                     field_type_def,
 759 |                                     field_selection_set,
 760 |                                     depth_limit.decrement(),
 761 |                                 );
 762 |                             } else {
 763 |                                 tracing::error!("field type {} not found", field_type_name);
 764 |                             }
 765 | 
 766 |                             field_type.arguments.iter().for_each(|arg| {
 767 |                                 let arg_type_name = arg.ty.inner_named_type();
 768 |                                 if let Some(arg_type) = tree_shaker.schema.types.get(arg_type_name)
 769 |                                 {
 770 |                                     retain_type(
 771 |                                         tree_shaker,
 772 |                                         arg_type,
 773 |                                         None,
 774 |                                         depth_limit.decrement(),
 775 |                                     );
 776 |                                 } else {
 777 |                                     tracing::error!(
 778 |                                         "field argument type {} not found",
 779 |                                         arg_type_name
 780 |                                     );
 781 |                                 }
 782 |                             });
 783 |                         } else {
 784 |                             tracing::error!("field {} not found", field_name);
 785 |                         }
 786 | 
 787 |                         if let Some(field_definition_directives) =
 788 |                             field_definition.map(|f| f.directives.clone())
 789 |                         {
 790 |                             field_definition_directives.iter().for_each(|directive| {
 791 |                                 retain_directive(tree_shaker, directive.name.as_str(), depth_limit);
 792 |                             })
 793 |                         }
 794 |                         if let Some(field_selection_directives) = field_selection_directives {
 795 |                             field_selection_directives.iter().for_each(|directive| {
 796 |                                 retain_directive(tree_shaker, directive.name.as_str(), depth_limit);
 797 |                             })
 798 |                         }
 799 |                     },
 800 |                 );
 801 |         }
 802 |         ExtendedType::Union(union_def) => union_def.members.iter().for_each(|member| {
 803 |             if let Some(member_type) = tree_shaker.schema.types.get(member.as_str()) {
 804 |                 let member_selection_set = selection_set
 805 |                     .map(|selection_set| {
 806 |                         selection_set
 807 |                             .clone()
 808 |                             .into_iter()
 809 |                             .filter(|selection| match selection {
 810 |                                 Selection::Field(_) => true,
 811 |                                 Selection::FragmentSpread(fragment) => {
 812 |                                     if let Some(fragment_def) = &tree_shaker
 813 |                                         .named_fragments
 814 |                                         .get(fragment.fragment_name.as_str())
 815 |                                     {
 816 |                                         fragment_def.type_condition == member.as_str()
 817 |                                     } else {
 818 |                                         tracing::error!(
 819 |                                             "fragment {} not found",
 820 |                                             fragment.fragment_name
 821 |                                         );
 822 |                                         false
 823 |                                     }
 824 |                                 }
 825 |                                 Selection::InlineFragment(fragment) => fragment
 826 |                                     .type_condition
 827 |                                     .clone()
 828 |                                     .is_none_or(|type_condition| {
 829 |                                         type_condition.as_str() == member.as_str()
 830 |                                     }),
 831 |                             })
 832 |                             .collect::<Vec<Selection>>()
 833 |                     })
 834 |                     .and_then(|s| if s.is_empty() { None } else { Some(s) });
 835 | 
 836 |                 if selection_set.is_none() || member_selection_set.is_some() {
 837 |                     retain_type(
 838 |                         tree_shaker,
 839 |                         member_type,
 840 |                         member_selection_set.as_ref(),
 841 |                         depth_limit.decrement(),
 842 |                     );
 843 |                 }
 844 |             } else {
 845 |                 tracing::error!("union member {} not found", member);
 846 |             }
 847 |         }),
 848 |         ExtendedType::Enum(def) => def.values.iter().for_each(|(_name, value)| {
 849 |             value.directives.iter().for_each(|directive| {
 850 |                 retain_directive(tree_shaker, directive.name.as_str(), depth_limit);
 851 |             })
 852 |         }),
 853 |         ExtendedType::Scalar(_) => {}
 854 |         ExtendedType::InputObject(input_def) => {
 855 |             input_def
 856 |                 .fields
 857 |                 .iter()
 858 |                 .for_each(|(_name, field_definition)| {
 859 |                     let field_type_name = field_definition.ty.inner_named_type();
 860 |                     if let Some(field_type_def) = tree_shaker.schema.types.get(field_type_name) {
 861 |                         retain_type(tree_shaker, field_type_def, None, depth_limit.decrement());
 862 |                     } else {
 863 |                         tracing::error!("field type {} not found", field_type_name);
 864 |                     }
 865 |                     field_definition.directives.iter().for_each(|directive| {
 866 |                         retain_directive(tree_shaker, directive.name.as_str(), depth_limit)
 867 |                     });
 868 |                 });
 869 |         }
 870 |     }
 871 | }
 872 | 
 873 | fn retain_directive(
 874 |     tree_shaker: &mut SchemaTreeShaker,
 875 |     directive_name: &str,
 876 |     depth_limit: DepthLimit,
 877 | ) {
 878 |     if let Some(tree_directive_node) = tree_shaker.directive_nodes.get_mut(directive_name) {
 879 |         tree_directive_node.retain = true;
 880 |         tree_directive_node.node.arguments.iter().for_each(|arg| {
 881 |             let arg_type_name = arg.ty.inner_named_type();
 882 |             if let Some(arg_type) = tree_shaker.schema.types.get(arg_type_name) {
 883 |                 retain_type(tree_shaker, arg_type, None, depth_limit.decrement())
 884 |             } else {
 885 |                 tracing::error!("argument type {} not found", arg_type_name);
 886 |             }
 887 |         });
 888 |     }
 889 | }
 890 | 
 891 | #[cfg(test)]
 892 | mod test {
 893 |     use apollo_compiler::{ast::OperationType, parser::Parser};
 894 |     use rstest::{fixture, rstest};
 895 | 
 896 |     use crate::{
 897 |         operations::operation_defs,
 898 |         schema_tree_shake::{DepthLimit, SchemaTreeShaker},
 899 |     };
 900 | 
 901 |     #[test]
 902 |     fn should_remove_type_mutation_mode_none() {
 903 |         let source_text = r#"
 904 |             type Query { id: String }
 905 |             type Mutation { id: String }
 906 |             type Subscription { id: String }
 907 |         "#;
 908 |         let document = Parser::new()
 909 |             .parse_ast(source_text, "schema.graphql")
 910 |             .unwrap();
 911 |         let schema = document.to_schema_validate().unwrap();
 912 |         let mut shaker = SchemaTreeShaker::new(&schema);
 913 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Unlimited);
 914 |         assert_eq!(
 915 |             shaker.shaken().unwrap().to_string(),
 916 |             "type Query {\n  id: String\n}\n"
 917 |         );
 918 |     }
 919 | 
 920 |     #[test]
 921 |     fn should_remove_type_mutation_mode_all() {
 922 |         let source_text = r#"
 923 |             type Query { id: String }
 924 |             type Mutation { id: String }
 925 |             type Subscription { id: String }
 926 |         "#;
 927 |         let document = Parser::new()
 928 |             .parse_ast(source_text, "schema.graphql")
 929 |             .unwrap();
 930 |         let schema = document.to_schema_validate().unwrap();
 931 |         let mut shaker = SchemaTreeShaker::new(&schema);
 932 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Unlimited);
 933 |         shaker.retain_operation_type(OperationType::Mutation, None, DepthLimit::Unlimited);
 934 |         assert_eq!(
 935 |             shaker.shaken().unwrap().to_string(),
 936 |             "type Query {\n  id: String\n}\n\ntype Mutation {\n  id: String\n}\n"
 937 |         );
 938 |     }
 939 | 
 940 |     #[test]
 941 |     fn should_remove_custom_names_mutation_mode_none() {
 942 |         let source_text = r#"
 943 |             schema {
 944 |               query: CustomQuery,
 945 |               mutation: CustomMutation,
 946 |               subscription: CustomSubscription
 947 |             }
 948 |             type CustomQuery { id: String }
 949 |             type CustomMutation { id: String }
 950 |             type CustomSubscription { id: String }
 951 |         "#;
 952 |         let document = Parser::new()
 953 |             .parse_ast(source_text, "schema.graphql")
 954 |             .unwrap();
 955 |         let schema = document.to_schema_validate().unwrap();
 956 |         let mut shaker = SchemaTreeShaker::new(&schema);
 957 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Unlimited);
 958 |         assert_eq!(
 959 |             shaker.shaken().unwrap().to_string(),
 960 |             "schema {\n  query: CustomQuery\n}\n\ntype CustomQuery {\n  id: String\n}\n"
 961 |         );
 962 |     }
 963 | 
 964 |     #[test]
 965 |     fn should_remove_custom_names_mutation_mode_all() {
 966 |         let source_text = r#"
 967 |             schema {
 968 |               query: CustomQuery,
 969 |               mutation: CustomMutation,
 970 |               subscription: CustomSubscription
 971 |             }
 972 |             type CustomQuery { id: String }
 973 |             type CustomMutation { id: String }
 974 |             type CustomSubscription { id: String }
 975 |         "#;
 976 |         let document = Parser::new()
 977 |             .parse_ast(source_text, "schema.graphql")
 978 |             .unwrap();
 979 |         let schema = document.to_schema_validate().unwrap();
 980 |         let mut shaker = SchemaTreeShaker::new(&schema);
 981 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Unlimited);
 982 |         shaker.retain_operation_type(OperationType::Mutation, None, DepthLimit::Unlimited);
 983 |         assert_eq!(
 984 |             shaker.shaken().unwrap().to_string(),
 985 |             "schema {\n  query: CustomQuery\n  mutation: CustomMutation\n}\n\ntype CustomQuery {\n  id: String\n}\n\ntype CustomMutation {\n  id: String\n}\n"
 986 |         );
 987 |     }
 988 | 
 989 |     #[test]
 990 |     fn should_remove_orphan_types() {
 991 |         let source_text = r#"
 992 |             type Query { id: UsedInQuery }
 993 |             type Mutation { id: UsedInMutation }
 994 |             type Subscription { id: UsedInSubscription }
 995 |             scalar UsedInQuery
 996 |             type UsedInMutation { id: String }
 997 |             enum UsedInSubscription { VALUE }
 998 |         "#;
 999 |         let document = Parser::new()
1000 |             .parse_ast(source_text, "schema.graphql")
1001 |             .unwrap();
1002 |         let schema = document.to_schema_validate().unwrap();
1003 |         let mut shaker = SchemaTreeShaker::new(&schema);
1004 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Unlimited);
1005 |         shaker.retain_operation_type(OperationType::Mutation, None, DepthLimit::Unlimited);
1006 |         assert_eq!(
1007 |             shaker.shaken().unwrap().to_string(),
1008 |             "type Query {\n  id: UsedInQuery\n}\n\ntype Mutation {\n  id: UsedInMutation\n}\n\nscalar UsedInQuery\n\ntype UsedInMutation {\n  id: String\n}\n"
1009 |         );
1010 |     }
1011 | 
1012 |     #[test]
1013 |     fn should_work_with_selection_set() {
1014 |         let source_text = r#"
1015 |             type Query { id: UsedInQuery unused: UsedInQueryButUnusedField }
1016 |             type Mutation { id: UsedInMutation }
1017 |             type Subscription { id: UsedInSubscription }
1018 |             scalar UsedInQuery
1019 |             type UsedInQueryButUnusedField { id: String, unused: String }
1020 |             type UsedInMutation { id: String }
1021 |             enum UsedInSubscription { VALUE }
1022 |         "#;
1023 |         let document = Parser::new()
1024 |             .parse_ast(source_text, "schema.graphql")
1025 |             .unwrap();
1026 |         let schema = document.to_schema_validate().unwrap();
1027 |         let mut shaker = SchemaTreeShaker::new(&schema);
1028 |         let (operation_document, operation_def, _comments) = operation_defs(
1029 |             "query TestQuery { id }",
1030 |             false,
1031 |             Some("operation.graphql".to_string()),
1032 |         )
1033 |         .unwrap()
1034 |         .unwrap();
1035 |         shaker.retain_operation(&operation_def, &operation_document, DepthLimit::Unlimited);
1036 |         assert_eq!(
1037 |             shaker.shaken().unwrap().to_string(),
1038 |             "type Query {\n  id: UsedInQuery\n}\n\nscalar UsedInQuery\n"
1039 |         );
1040 |     }
1041 | 
1042 |     #[fixture]
1043 |     fn nested_schema() -> apollo_compiler::Schema {
1044 |         Parser::new()
1045 |             .parse_ast(
1046 |                 r#"
1047 |                     type Query  { level1: Level1 }
1048 |                     type Level1 { level2: Level2 }
1049 |                     type Level2 { level3: Level3 }
1050 |                     type Level3 { level4: Level4 }
1051 |                     type Level4 { id: String }
1052 |                 "#,
1053 |                 "schema.graphql",
1054 |             )
1055 |             .unwrap()
1056 |             .to_schema_validate()
1057 |             .unwrap()
1058 |             .into_inner()
1059 |     }
1060 | 
1061 |     #[rstest]
1062 |     fn should_respect_depth_limit(nested_schema: apollo_compiler::Schema) {
1063 |         let mut shaker = SchemaTreeShaker::new(&nested_schema);
1064 | 
1065 |         // Get the Query type to start from
1066 |         let query_type = nested_schema.types.get("Query").unwrap();
1067 | 
1068 |         // Test with depth limit of 1
1069 |         shaker.retain_type(query_type, None, DepthLimit::Limited(1));
1070 |         let shaken_schema = shaker.shaken().unwrap();
1071 | 
1072 |         // Should retain only Query, not Level1, Level2, Level3, or Level4
1073 |         assert!(shaken_schema.types.contains_key("Query"));
1074 |         assert!(!shaken_schema.types.contains_key("Level1"));
1075 |         assert!(!shaken_schema.types.contains_key("Level2"));
1076 |         assert!(!shaken_schema.types.contains_key("Level3"));
1077 |         assert!(!shaken_schema.types.contains_key("Level4"));
1078 | 
1079 |         // Test with depth limit of 2
1080 |         let mut shaker = SchemaTreeShaker::new(&nested_schema);
1081 |         shaker.retain_type(query_type, None, DepthLimit::Limited(2));
1082 |         let shaken_schema = shaker.shaken().unwrap();
1083 | 
1084 |         // Should retain Query and Level1, but not deeper levels
1085 |         assert!(shaken_schema.types.contains_key("Query"));
1086 |         assert!(shaken_schema.types.contains_key("Level1"));
1087 |         assert!(!shaken_schema.types.contains_key("Level2"));
1088 |         assert!(!shaken_schema.types.contains_key("Level3"));
1089 |         assert!(!shaken_schema.types.contains_key("Level4"));
1090 | 
1091 |         // Test with depth limit of 1 starting from Level2
1092 |         let mut shaker = SchemaTreeShaker::new(&nested_schema);
1093 |         let level2_type = nested_schema.types.get("Level2").unwrap();
1094 |         shaker.retain_type(level2_type, None, DepthLimit::Limited(1));
1095 |         let shaken_schema = shaker.shaken().unwrap();
1096 | 
1097 |         // Should retain only Level2
1098 |         assert!(!shaken_schema.types.contains_key("Level1"));
1099 |         assert!(shaken_schema.types.contains_key("Level2"));
1100 |         assert!(!shaken_schema.types.contains_key("Level3"));
1101 |         assert!(!shaken_schema.types.contains_key("Level4"));
1102 | 
1103 |         // Test with depth limit of 2 starting from Level2
1104 |         let mut shaker = SchemaTreeShaker::new(&nested_schema);
1105 |         shaker.retain_type(level2_type, None, DepthLimit::Limited(2));
1106 |         let shaken_schema = shaker.shaken().unwrap();
1107 | 
1108 |         // Should retain Level2 and Level3
1109 |         assert!(!shaken_schema.types.contains_key("Level1"));
1110 |         assert!(shaken_schema.types.contains_key("Level2"));
1111 |         assert!(shaken_schema.types.contains_key("Level3"));
1112 |         assert!(!shaken_schema.types.contains_key("Level4"));
1113 | 
1114 |         // Test with depth limit of 5 starting from Level2
1115 |         let mut shaker = SchemaTreeShaker::new(&nested_schema);
1116 |         shaker.retain_type(level2_type, None, DepthLimit::Limited(5));
1117 |         let shaken_schema = shaker.shaken().unwrap();
1118 | 
1119 |         // Should retain Level2 and deeper types
1120 |         assert!(!shaken_schema.types.contains_key("Level1"));
1121 |         assert!(shaken_schema.types.contains_key("Level2"));
1122 |         assert!(shaken_schema.types.contains_key("Level3"));
1123 |         assert!(shaken_schema.types.contains_key("Level4"));
1124 |     }
1125 | 
1126 |     #[rstest]
1127 |     fn should_retain_all_types_with_unlimited_depth(nested_schema: apollo_compiler::Schema) {
1128 |         let mut shaker = SchemaTreeShaker::new(&nested_schema);
1129 | 
1130 |         // Get the Query type to start from
1131 |         let query_type = nested_schema.types.get("Query").unwrap();
1132 | 
1133 |         // Test with unlimited depth
1134 |         shaker.retain_type(query_type, None, DepthLimit::Unlimited);
1135 |         let shaken_schema = shaker.shaken().unwrap();
1136 | 
1137 |         // Should retain all types
1138 |         assert!(shaken_schema.types.contains_key("Query"));
1139 |         assert!(shaken_schema.types.contains_key("Level1"));
1140 |         assert!(shaken_schema.types.contains_key("Level2"));
1141 |         assert!(shaken_schema.types.contains_key("Level3"));
1142 |         assert!(shaken_schema.types.contains_key("Level4"));
1143 |     }
1144 | 
1145 |     #[test]
1146 |     fn should_work_with_recursive_schemas() {
1147 |         let source_text = r#"
1148 |             type Query { id: TypeA }
1149 |             type TypeA { id: TypeB }
1150 |             type TypeB { id: TypeA }
1151 |         "#;
1152 |         let document = Parser::new()
1153 |             .parse_ast(source_text, "schema.graphql")
1154 |             .unwrap();
1155 |         let schema = document.to_schema_validate().unwrap();
1156 |         let mut shaker = SchemaTreeShaker::new(&schema);
1157 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Unlimited);
1158 |         assert_eq!(
1159 |             shaker.shaken().unwrap().to_string(),
1160 |             "type Query {\n  id: TypeA\n}\n\ntype TypeA {\n  id: TypeB\n}\n\ntype TypeB {\n  id: TypeA\n}\n"
1161 |         );
1162 |     }
1163 | 
1164 |     #[test]
1165 |     fn should_work_with_recursive_and_depth() {
1166 |         let source_text = r#"
1167 |             type Query { field1: TypeA, field2: TypeB }
1168 |             type TypeA { id: TypeB }
1169 |             type TypeB { id: TypeC }
1170 |             type TypeC { id: TypeA }
1171 |         "#;
1172 |         let document = Parser::new()
1173 |             .parse_ast(source_text, "schema.graphql")
1174 |             .unwrap();
1175 |         let schema = document.to_schema_validate().unwrap();
1176 |         let mut shaker = SchemaTreeShaker::new(&schema);
1177 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Limited(3));
1178 |         assert_eq!(
1179 |             shaker.shaken().unwrap().to_string(),
1180 |             "type Query {\n  field1: TypeA\n  field2: TypeB\n}\n\ntype TypeA {\n  id: TypeB\n}\n\ntype TypeB {\n  id: TypeC\n}\n\ntype TypeC {\n  id: TypeA\n}\n"
1181 |         );
1182 |     }
1183 | 
1184 |     #[test]
1185 |     fn should_retain_builtin_directives() {
1186 |         let source_text = r#"
1187 |             type Query {
1188 |                 field1: String @deprecated(reason: "Use 'field2' instead")
1189 |                 field2: String
1190 |             }
1191 |         "#;
1192 |         let document = Parser::new()
1193 |             .parse_ast(source_text, "schema.graphql")
1194 |             .unwrap();
1195 |         let schema = document.to_schema_validate().unwrap();
1196 |         let mut shaker = SchemaTreeShaker::new(&schema);
1197 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Limited(3));
1198 |         assert_eq!(
1199 |             shaker.shaken().unwrap().to_string(),
1200 |             "type Query {\n  field1: String @deprecated(reason: \"Use 'field2' instead\")\n  field2: String\n}\n"
1201 |         );
1202 |     }
1203 | 
1204 |     #[test]
1205 |     fn should_retain_custom_directives() {
1206 |         let source_text = r#"
1207 |             type Query {
1208 |                 field1: String @CustomDirective(arg: "Use 'field2' instead")
1209 |                 field2: String
1210 |             }
1211 |             directive @CustomDirective(arg: CustomScalar) on FIELD_DEFINITION
1212 |             scalar CustomScalar
1213 |         "#;
1214 |         let document = Parser::new()
1215 |             .parse_ast(source_text, "schema.graphql")
1216 |             .unwrap();
1217 |         let schema = document.to_schema_validate().unwrap();
1218 |         let mut shaker = SchemaTreeShaker::new(&schema);
1219 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Limited(3));
1220 |         assert_eq!(
1221 |             shaker.shaken().unwrap().to_string(),
1222 |             "directive @CustomDirective(arg: CustomScalar) on FIELD_DEFINITION\n\ntype Query {\n  field1: String @CustomDirective(arg: \"Use 'field2' instead\")\n  field2: String\n}\n\nscalar CustomScalar\n"
1223 |         );
1224 |     }
1225 | 
1226 |     #[test]
1227 |     fn recursive_input() {
1228 |         let source_text = r#"
1229 |             input Filter {
1230 |                 field: String
1231 |                 filter: Filter
1232 |             }
1233 |             type Query {
1234 |                 field(filter: Filter): String
1235 |             }
1236 |         "#;
1237 |         let document = Parser::new()
1238 |             .parse_ast(source_text, "schema.graphql")
1239 |             .unwrap();
1240 |         let schema = document.to_schema_validate().unwrap();
1241 |         let mut shaker = SchemaTreeShaker::new(&schema);
1242 |         shaker.retain_operation_type(OperationType::Query, None, DepthLimit::Unlimited);
1243 |         assert_eq!(
1244 |             shaker.shaken().unwrap().to_string(),
1245 |             "input Filter {\n  field: String\n  filter: Filter\n}\n\ntype Query {\n  field(filter: Filter): String\n}\n"
1246 |         );
1247 |     }
1248 | 
1249 |     #[test]
1250 |     fn should_retain_field_argument_descriptions() {
1251 |         let source_text = r#"
1252 |             type Query {
1253 |                 someQuery(""" an id """ id: ID!, """ other arg """ other: String): OutoutType
1254 |             }
1255 | 
1256 |             type OutoutType {
1257 |                 value: String
1258 |             }
1259 |         "#;
1260 |         let document = Parser::new()
1261 |             .parse_ast(source_text, "schema.graphql")
1262 |             .unwrap();
1263 |         let schema = document.to_schema_validate().unwrap();
1264 |         let mut shaker = SchemaTreeShaker::new(&schema);
1265 |         let (operation_document, operation_def, _comments) = operation_defs(
1266 |             "query TestQuery($id1: ID, $other: String) { \
1267 |                 someQuery(id: $id1, other: $other, otherArg: $other) { \
1268 |                     value
1269 |                 }
1270 |             }",
1271 |             false,
1272 |             Some("operation.graphql".to_string()),
1273 |         )
1274 |         .unwrap()
1275 |         .unwrap();
1276 |         shaker.retain_operation(&operation_def, &operation_document, DepthLimit::Unlimited);
1277 | 
1278 |         let id_description = shaker.arguments_descriptions.get("id1");
1279 |         let other_description = shaker.arguments_descriptions.get("other");
1280 | 
1281 |         assert_eq!(shaker.arguments_descriptions.len(), 2);
1282 |         assert!(id_description.is_some());
1283 |         assert_eq!(*id_description.unwrap(), vec!["an id"]);
1284 |         assert!(other_description.is_some());
1285 |         assert_eq!(*other_description.unwrap(), vec!["other arg"]);
1286 |     }
1287 | 
1288 |     #[test]
1289 |     fn should_retain_field_argument_descriptions_when_multiple_are_found() {
1290 |         let source_text = r#"
1291 |             type Query {
1292 |                 someQuery(""" an id """ id: ID!, """ other arg """ other: String, """ another arg """ otherArg: String): OutoutType
1293 |                 someQuery2(""" another id """ id: ID!, """ arg 2 """ other: String): OutoutType
1294 |             }
1295 | 
1296 |             type OutoutType {
1297 |                 value: String
1298 |             }
1299 |         "#;
1300 |         let document = Parser::new()
1301 |             .parse_ast(source_text, "schema.graphql")
1302 |             .unwrap();
1303 |         let schema = document.to_schema_validate().unwrap();
1304 |         let mut shaker = SchemaTreeShaker::new(&schema);
1305 |         let (operation_document, operation_def, _comments) = operation_defs(
1306 |             "query TestQuery($id: ID, $other2: String) { \
1307 |                 someQuery(id: $id, other: $other2, otherArg: $other2) { \
1308 |                     value
1309 |                 }
1310 |             }",
1311 |             false,
1312 |             Some("operation.graphql".to_string()),
1313 |         )
1314 |         .unwrap()
1315 |         .unwrap();
1316 |         shaker.retain_operation(&operation_def, &operation_document, DepthLimit::Unlimited);
1317 | 
1318 |         let id_description = shaker.arguments_descriptions.get("id");
1319 |         let other_description = shaker.arguments_descriptions.get("other2");
1320 | 
1321 |         assert_eq!(shaker.arguments_descriptions.len(), 2);
1322 |         assert!(id_description.is_some());
1323 |         assert_eq!(*id_description.unwrap(), vec!["an id"]);
1324 |         assert!(other_description.is_some());
1325 |         assert_eq!(
1326 |             *other_description.unwrap(),
1327 |             vec!["other arg", "another arg"]
1328 |         );
1329 |     }
1330 | 
1331 |     #[test]
1332 |     fn should_retain_builtin_directive_argument_descriptions() {
1333 |         let source_text = r#"
1334 |             type Query {
1335 |                 someQuery(id: ID!, other: Boolean!): OutoutType
1336 |             }
1337 | 
1338 |             type OutoutType {
1339 |                 id: ID!
1340 |                 value: String
1341 |             }
1342 |         "#;
1343 |         let document = Parser::new()
1344 |             .parse_ast(source_text, "schema.graphql")
1345 |             .unwrap();
1346 |         let schema = document.to_schema_validate().unwrap();
1347 |         let mut shaker = SchemaTreeShaker::new(&schema);
1348 |         let (operation_document, operation_def, _comments) = operation_defs(
1349 |             "query TestQuery($id: ID, $other: Boolean!) { \
1350 |                 someQuery(id: $id, other: $other) { \
1351 |                     id
1352 |                     value @skip(if: $other)
1353 |                 }
1354 |             }",
1355 |             false,
1356 |             Some("operation.graphql".to_string()),
1357 |         )
1358 |         .unwrap()
1359 |         .unwrap();
1360 |         shaker.retain_operation(&operation_def, &operation_document, DepthLimit::Unlimited);
1361 | 
1362 |         let description = shaker.arguments_descriptions.get("other");
1363 | 
1364 |         assert!(description.is_some());
1365 |         assert_eq!(*description.unwrap(), vec!["Skipped when true."]);
1366 |     }
1367 | 
1368 |     #[test]
1369 |     fn should_retain_custom_directive_argument_descriptions() {
1370 |         let source_text = r#"
1371 |             type Query {
1372 |                 someQuery(id: ID!, other: Boolean!): OutoutType
1373 |             }
1374 | 
1375 |             directive @x(""" the value """ value: String) on FIELD_DEFINITION
1376 | 
1377 |             type OutoutType {
1378 |                 id: ID!
1379 |                 value: String
1380 |             }
1381 |         "#;
1382 |         let document = Parser::new()
1383 |             .parse_ast(source_text, "schema.graphql")
1384 |             .unwrap();
1385 |         let schema = document.to_schema_validate().unwrap();
1386 |         let mut shaker = SchemaTreeShaker::new(&schema);
1387 |         let (operation_document, operation_def, _comments) = operation_defs(
1388 |             "query TestQuery($id: ID, $other: Boolean!) { \
1389 |                 someQuery(id: $id, other: $other) { \
1390 |                     id
1391 |                     value @x(value: $other)
1392 |                 }
1393 |             }",
1394 |             false,
1395 |             Some("operation.graphql".to_string()),
1396 |         )
1397 |         .unwrap()
1398 |         .unwrap();
1399 |         shaker.retain_operation(&operation_def, &operation_document, DepthLimit::Unlimited);
1400 | 
1401 |         let description = shaker.arguments_descriptions.get("other");
1402 | 
1403 |         assert!(description.is_some());
1404 |         assert_eq!(*description.unwrap(), vec!["the value"]);
1405 |     }
1406 | }
1407 | 
```
Page 6/8FirstPrevNextLast