Skip to main content

Class 4b: Dynamic Discovery and the Search / Introspect / Execute Pattern

Duration: 30 minutes | Difficulty: Intermediate | Prerequisites: Class 4.

This lesson is an optional detour between Class 4 and Class 5. If your team has already decided to ship a curated-operations directory, skip it and continue to Class 5.


Companion code

This lesson carries on from Class 4. If you followed along, keep working on the project you already have. If you skipped it, clone the mcp_class_4 branch to start from the same place:

git clone --branch mcp_class_4 https://github.com/graphqlguy/graphQLMovieDB-MCP.git

By the end of this lesson you will know the main alternative to the curated-operations pattern Class 4 walked through. It is the three-tool dynamic-discovery design, first described by Grafbase and shipped as built-in tools in Apollo MCP Server. You will also be able to decide whether your team should use it.

In Class 1 we said that a live GraphQL endpoint is a design-time tool for human developers, and that agents should not use it at runtime. There are three reasons.

The first is cost. An LLM cannot tell which queries are expensive. It will request a deeply nested query because the schema allows it, and then retry again and again after the server rejects it.

The second is security. The agent can ask for every field the schema exposes, including a password hash on User or a payoutsAccount on Vendor.

The third is volume. A human developer explores your API once during integration and then writes one working request. An agent explores every nullable field and retries on every error, each time it works on a problem.

This lesson covers a design between two extremes, a live GraphQL endpoint and a curated operations directory. Instead of either, the agent gets three meta-tools that let it explore the schema safely.

This lesson uses several terms that may be new. Here is a short definition of each, and each one gets a fuller explanation where the lesson puts it to work:

  • The search / introspect / execute pattern gives the agent three meta-tools, tools that work on the schema itself, so it can find the part of a schema it needs and write its own operation.
  • Dynamic discovery is Apollo MCP Server's name for that pattern, which you turn on with operations.source: introspect.
  • Introspection is asking a GraphQL server to describe its own schema, in a query, at runtime.
  • Schema Definition Language (SDL) is the text form of a GraphQL schema, the type Movie { ... } syntax.
  • Supergraph is the single schema that Apollo Federation composes from many subgraph services.
  • Tantivy is a full-text search library written in Rust, which Apollo MCP Server and Grafbase use to index a schema.
  • BM25 is a ranking function that scores a match by the words it shares with the search terms, using word statistics alone.
  • Minify is an Apollo MCP Server option that returns schema results in a compact notation, so they use fewer tokens.
  • Persisted-query manifest is a file listing the approved operations a server may run.
  • Semantic introspection is a GraphQL AI Working Group proposal to add a search field to the introspection schema itself.
  • MCP Inspector is the MCP project's developer tool for connecting to a server and calling its tools by hand.

Why a Third Mode Exists​

Class 4 pointed Apollo MCP Server at a directory of .graphql files. Class 5 will mention that the same server can be pointed at a persisted-query manifest published through GraphOS. Both follow the same idea: the team that owns the schema decides in advance exactly which operations the agent may call. The agent gets a short, reviewed list of typed operations, and the list changes only when the team changes it.

That model works well on schemas the size of this tutorial's, with a few dozen types and a few dozen operations. It gets harder on real supergraphs. A large enterprise supergraph can carry thousands of types and tens of thousands of fields across dozens of subgraphs. Its full introspection result fills a large part of an LLM's context window before the user has asked anything. Turning a schema that big into a static operations directory is a full-time job. And an agent asked to "find me the order history for customer 1234" has trouble finding the right operation among several variants spread across subgraphs.

The dynamic-discovery pattern addresses this problem. The agent gets a way to search the schema for the part it needs. The pattern is named after its three tools, search / introspect / execute, and Grafbase first described it in the post Solving context explosion in GraphQL MCP servers. Apollo MCP Server ships the same design as built-in introspection tools. They arrived before 1.0 and reached with 1.0 in October 2025.

Other implementations have their own variants, among them IBM API Connect for GraphQL (the StepZen line, which generates MCP servers from GraphQL APIs) and Microsoft's MCP integration for Fabric's GraphQL API. The widely installed open-source mcp-graphql server works differently. It ships two tools, introspect-schema and query-graphql. The first returns the whole schema, and the second runs the query, without a search or validation step in between.

Lesser-known fact

The search/introspect/execute design mirrors how a human developer uses GraphiQL or Apollo Studio: open the explorer, type a few characters into the schema search, expand the type that looks promising, read its fields, then write the query. The MCP version gives an agent the same workflow through three meta-tools, where a person uses a UI. The steps come in the same order:

  1. find the area of the schema that matters
  2. read the details of what you found
  3. write the operation

The Three Tools​

A dynamic-discovery MCP server exposes exactly three tools to the agent, with these shapes.

search(terms: string[]) returns the smallest schema slice relevant to a set of search terms. The argument is an array of separate words, so the caller splits the user's request into words first.

The server matches the terms against a pre-built index of the schema's types, fields, and descriptions. It returns the top matches as a chunk of the agent can read, along with the parent-type information needed to construct an operation. Both Apollo MCP Server and Grafbase build this index with tantivy, a Rust full-text search library, instead of vector embeddings. Hot Chocolate v16's semantic introspection defaults to lexical ranking as well. A GraphQL-tuned embedding model for schema retrieval was announced on graphql.org in July 2026, and is still entering the AI Working Group through open pull requests. Production servers today run full-text indexes. For a prompt like "find an order by its customer's email address," search might return the Order type, the Customer type, the Query.findOrdersByCustomerEmail operation, and one or two related types like OrderStatus. That is enough for the agent to write a query against that area, and the agent sees only that part of the supergraph.

introspect(type_name: string, depth: int) is the follow-up. Once search has shown the agent which area of the schema is interesting, introspect lets the agent ask for the full definition of one specific type. The response is the SDL for that type plus its directly referenced types, recursed as far as depth allows; Apollo defaults to one level. The agent calls it when it has chosen a type, such as Order, and wants everything about it.

The minify option produces a compact notation. Apollo puts its legend in the tool description, so the model learns the notation when it reads the tool list. The letters are:

  • T type, I input, E enum, U union, F interface
  • s String, i Int, f Float, b Boolean, d ID
  • @D a deprecation

execute(query: string, variables: string) runs the resulting GraphQL operation. Note the second parameter's type: Apollo takes the variables as a JSON string, not as a nested JSON object. That matters when you test a call by hand in MCP Inspector. By this point, the agent has written a query against the part of the schema it found through search and introspect. The server validates the document against the live schema, applies any operational policies (depth limits, complexity caps, allow-list filtering, per-user authorization), and executes against the real backend. From the resolvers' point of view, the request looks like any normal GraphQL request.

Those parameter names are Apollo MCP Server 1.17.0's, and they are what you will type into MCP Inspector later in this lesson. Three tools is the standard design, but the names vary. Other implementations rename the tools or their parameters, and some add a fourth. Apollo ships validate(operation: string), and others add a list_operations tool that lists known operations. The structure stays the same: a search tool, a tool that expands one type, and an executor.

Why exactly three, not one

You could combine all three into a single query_graphql(prompt: string) tool that searches and executes in one call. Several early implementations tried that. Implementations settled on three separate tools because the model does better when it sees the intermediate results. After search, the agent reads the matched SDL and checks whether it found the right area.

After introspect, it reads the full type definition and decides which fields to ask for. After execute, it reads the result and decides what to tell the user. Each step gives the model a chance to notice a mistake. A single combined tool hides those results. If its search picks the wrong part of the schema, the model cannot go back, and the wrong query runs at full cost. Three tools make each step slower, and the whole task runs fewer wrong queries.

Wiring It Up in Apollo MCP Server​

Apollo MCP Server 1.0 ships the dynamic-discovery tools as built-in introspection tools. You configure them in an introspection section of the server's config file, and the operations directory stays unused. Only two sections differ from the Class 4 setup, so write this mode as a second config file next to the first one. Class 5 continues from the curated config. With both files, you switch modes by passing a different file on the command line. This config was checked against v1.17.0. If keys change in a later release, the config file documentation is the reference:

apollo/mcp-config-dynamic.yaml
endpoint: http://localhost:8080/graphql

transport:
type: streamable_http
address: 127.0.0.1
port: 8000

schema:
source: local
path: ./schema.graphqls

operations:
source: introspect

introspection:
search: { enabled: true }
introspect: { enabled: true }
validate: { enabled: true }
execute: { enabled: true }

Stop the Class 4 server and start this one instead, from the same directory:

apollo-mcp-server mcp-config-dynamic.yaml

Three things change compared with the Class 4 setup.

First, instead of pointing the server at a directory of .graphql files, you enable its built-in introspection tools. The server connects to the live moviedb GraphQL endpoint, reads the schema, and exposes search, introspect, execute, and validate as tools. The agent writes operations at runtime, and the server validates them against the schema before execution.

Second, the search tool needs a schema index. Apollo MCP Server builds this with a tantivy full-text index over the schema's types, fields, and descriptions (the apollo-schema-index crate), constructed when the server starts. Two properties of that index affect what the agent gets back. First, it uses BM25 keyword scoring, not embeddings, so it matches only words that appear in the schema, even when another word means the same thing. That is why the argument is an array of words.

Second, the results stop at five matches. A constant in the source code sets that limit, and no configuration option changes it. On a schema with many similarly named types, the agent cannot reach the sixth-best match, whatever words it uses. Each match comes back as trimmed SDL that shows only the fields on the path to the match. A type can therefore appear with a single field, and the agent has to introspect it to see the rest.

The index is built locally, so you do not need an embedding model or an external vector store. Both search and introspect also support a minify option, which uses the compact notation to take less of the context window per result. It does reduce the total, with one problem. The minifier removes every whitespace character inside descriptions, instead of reducing each run of whitespace to one space, so words on either side of a line break are joined together.

A tokenizer that splits the schema into two common tokens sees theschema instead, and spends more tokens on it. So the description part of a minified result can cost more than the original text. The saving comes from the compact notation for types and fields, and the joined-up descriptions reduce it.

Third, execution limits become mandatory, and Apollo MCP Server leaves them to you. The server validates the agent's operations against the schema, but it does not limit query depth or complexity. Without a limit further down, the agent can write a query of any depth. Enforcement has to live in the layer that executes the query: graphql-java instrumentation limits in your Spring service, or Apollo Router if one fronts your graph.

The moviedb service already has part of this. The app ships a QueryDepthInstrumentation that rejects any query deeper than a configurable maximum before execution starts. The default is ten, raised to 15 in the app's configuration so the standard introspection query, at depth 13, still passes. It still lacks a complexity limit. Before you expose the server beyond your laptop, check that the limits you rely on are switched on.

With the introspection tools enabled, point MCP Inspector at the server and look at the tool list. Instead of RecommendMoviesForMood, SummarizeMovieReviews, and AddToWatchlist, you will see search, introspect, execute, and validate. The startup log confirms that the index is built and shows the schema's size: schema_index: Indexed 24 types in 2.65ms.

The flow, in four calls​

The tools are meant to be called in order. search narrows the schema to an area, introspect expands one type inside it, validate checks the composed operation against the schema, and execute runs it.

Only the last call reaches the backend. The first three read the schema, so the agent can explore, guess wrong and search again, at the cost of an index lookup each time. Each step below shows the exact arguments for its tool. In MCP Inspector, turn on Edit as JSON in the tool's form, paste the arguments into the box, and follow the flow one call at a time.

Step 1. search. Take the request "find anything called Godfather, whether it is a movie, a show, or a person" and break it into terms. The argument is an array of words, not a sentence, because the index does keyword scoring:

{ "terms": ["search", "movie", "show", "person", "union"] }

The result is a trimmed type Query that holds search(query: String!): [SearchResult!]!, person, the deprecated searchPeople, and all of recommendMoviesForMood with its description. Two things are called search here. The meta-tool search searches the schema, and Query.search searches the data. The agent uses the first to find the second.

Step 2. introspect. The result shows the operation's signature but stops at its return type, so ask for that type by name:

{ "type_name": "SearchResult", "depth": 2 }

The depth matters. At the default of 1, the whole response is the single line union SearchResult, which is too little for the agent to select fields. At 2 you get union SearchResult = Movie | Person | TvShow with the three member types. Only then does the agent learn that it needs inline fragments to select fields for each member.

Step 3. validate. Write the operation from what the first two calls returned, then check it before running it:

{ "operation": "query SearchMoviesPeopleAndTvShows($q: String!) { search(query: $q) { __typename ... on Movie { id title releaseYear } ... on Person { id name } ... on TvShow { id title startYear } } }" }

A document that checks out answers Operation is valid. The step is optional, because execute validates too. It is still useful: a failed validate costs one cheap round trip, and the same mistake in execute costs a real query against the backend.

Step 4. execute. Run the operation with its variables:

{
"query": "query SearchMoviesPeopleAndTvShows($q: String!) { search(query: $q) { __typename ... on Movie { id title releaseYear } ... on Person { id name } ... on TvShow { id title startYear } } }",
"variables": "{\"q\": \"Godfather\"}"
}

The variables value is a JSON string, not a nested JSON object, and that is the easiest detail to get wrong when you type a call by hand. Against the seeded catalog, the response is:

{"data":{"search":[
{"__typename":"Movie","id":"2","title":"The Godfather","releaseYear":1972},
{"__typename":"Movie","id":"3","title":"The Godfather Part II","releaseYear":1974},
{"__typename":"Movie","id":"4","title":"The Godfather Part III","releaseYear":1990}
]}}

The agent wrote the operation behind that result during those four calls, and no file on disk contains it. The rest of this section looks at a request where the first call goes badly.

Take a request a user might make: movies from the 1990s with a high rating. Broken into terms for the tool, that is ["movies", "released", "1990s", "high", "rating"]. The slice that comes back is a cut-down type Query holding moviesByIds, searchMovies, person, tmdbSearch, and recommendMoviesForMood with its full description attached. The slice leaves out Query.movies(filter: MovieFilter, page: Int, size: Int, sort: MovieSort): MoviePage!. That is the operation the request needs, and it is in the schema, with minYear, maxYear and minRating on its filter input.

This gap teaches more than anything else in the lesson. Ask search for MovieFilter by name, and it still does not come back. The reason is in the schema. movies lacks a description, because Class 3 wrote descriptions only for the fields it added, and left the rest as the Spring GraphQL tutorial had them. The tantivy index scores type names, field names and descriptions, so movies contributes only the word "movies". recommendMoviesForMood ranks higher, because its description repeats "movies", "rating", "search" and "recommendation". So whether the agent finds a field depends on whether that field has a description.

This is the real cost of the mode, and it is a different kind of cost from the trade-offs in the next section. With curated operations, a field without a description still works, because a person wrote the operation file, and the description only helps the agent choose between tools. With search/introspect/execute, the agent cannot find a field that lacks a description.

The failure is also silent. The agent finds something nearby and answers with that, so the user gets a plausible reply built on the wrong operation. Class 3's lint test only checks that a description exists. With dynamic discovery, a description decides whether the agent can find the field at all, and that applies to the whole schema, including the parts you did not write for agents.

The Godfather request from the steps above went better, because the fields it needed have descriptions. Its terms, ["search", "movie", "show", "person", "union"], returned a type Query slice that includes search(query: String!): [SearchResult!]!.

Look at why that slice contains what it does. union SearchResult = Movie | Person | TvShow comes back only because the terms include the word union, which appears in the union's declaration. Without that term, the result leaves out both Query.search and SearchResult. So the words the agent passes decide what it can see. When search does not return a type, introspect can expand it, and the three-tool design depends on that split between the two tools.

The agent found one operation on its own and missed the other, and the difference was a description. Nobody had to write an operation file such as movies_1990s_high_rated.graphql in advance for either.

The Trade-offs​

The pattern reaches further than curated operations, and it is also riskier. Three trade-offs decide whether your team should use it.

Cost analysis becomes harder. A curated operation is cost-analyzed once, when it is written, and its cost stays known. A dynamic operation is cost-analyzed at runtime, after the agent has already spent tokens on discovery. If the agent writes a query over the complexity limit, the GraphQL service or router rejects it, and the agent tries again. In the good case, the agent makes its query smaller and succeeds on the second try.

In the bad case, the agent decides the schema is broken and tries five variants. So the cost of dynamic discovery varies, and the cost of a curated operation stays the same. Apollo publishes ranges for its own server:

  • with search and minification on: three to six tool calls and 3,200 to 5,300 tokens per task
  • without them: ten to twelve calls and 5,000 to 9,500 tokens

Treat those as a rough guide to the cost, and avoid budgeting with them, because both ends change with the schema, the model and the question.

Security review moves from build time to runtime. A person can review a curated operation before it reaches production, and nobody can review a dynamic one in advance. The server does provide controls:

  • overrides.mutation_mode keeps mutations off until you turn them on.
  • Per-operation OAuth scopes under overrides.required_scopes make the server reject calls whose token lacks them.

Restricting individual fields is the GraphQL layer's job, by design. It happens in the @PreAuthorize checks your Spring services already run, and in what the schema exposes. Safety then depends on limiting what any operation can do, because nobody reviews each operation. That is a bigger change for your organization than the small config change suggests.

Schema drift starts to bite. When operations live in a directory, renaming a field breaks the operations, and CI catches it. When the agent writes operations at runtime, renaming a field breaks the agent's next call, and you find out in production, possibly while a customer is using it. The remedy is a practice you should follow anyway: deprecation cycles by schema coordinate, gradual migrations, and the patterns from Evolving GraphQL Schemas Without Breaking Everything. With dynamic discovery, skipping that practice causes problems sooner.

The table compares the three operation-source modes:

DimensionOperation files (Class 4)Persisted manifests (Class 5)Dynamic discovery (this lesson)
Where operations liveA directory of .graphqlPersisted-query manifest (local file or GraphOS uplink)Generated at runtime by the agent
Who reviews themCode review on the directoryCI pipeline publishing the manifestServer-side policies (no human)
Cost analysisAt commit timeAt publish timeAt execution time
Schema-drift detectionCI on the operations folderCI on the publishing stepProduction runtime
Right for schema sizeSmall to mediumSmall to largeLarge to very large
Right for agent autonomyConstrainedConstrainedExploratory
Setup effortLowestModerate (manifest tooling; GraphOS only for uplink)Highest (schema index plus policy config)
Token cost per taskLowest (one tool call)Lowest (one tool call)Higher (three to six tool calls)

Dynamic discovery is right when the schema is too big to curate, when the agent's queries are too varied to predict, or both. The moviedb schema is small and its queries are predictable, so curated operations are the right answer. For an enterprise supergraph with thousands of operations across dozens of subgraphs, curating stops being practical, and dynamic discovery is worth its complexity.

Do not switch on dynamic discovery without policies

Enabling Apollo MCP Server's introspection tools is a small configuration change, and three other things have to come with it:

  • execution limits in the GraphQL layer, meaning query depth and complexity limits in your Spring service or router, because only those layers enforce them
  • the server's mutation and scope controls
  • authorization

Treat those policies as part of the feature, and check the config reference for the options your server version supports.

Where Dynamic Discovery Is Heading​

Two developments may shape how dynamic discovery looks in 2027 and later.

The first is the GraphQL AI Working Group, announced in October 2025 after the interest in AI at GraphQLConf 2025. By mid-2026 its schema-navigation work had produced concrete results. The main one is the semantic introspection RFC. It proposes a standard search field inside the introspection schema, so the search meta-tool this lesson configures would become a feature of the schema itself. The RFC is a working document by Hot Chocolate's authors at ChilliCream, and it does not have a stage yet. Hot Chocolate v16 shipped an implementation in May 2026.

Alongside it, a GraphQL-tuned open embedding model for schema retrieval was announced on graphql.org in July 2026. It was evaluated against a benchmark of 816 natural-language questions over five production schemas. The model and the benchmark are entering the working group through open pull requests. Together they give the group a shared way to measure whether a schema-search approach finds the right fields. A @require_graphql_subschema directive, for declaring subschemas small enough to fit a context window, is still an exploratory idea in the group's issue tracker. If semantic introspection becomes part of the spec, some of the search work in this lesson moves from the server into the schema.

The second is Anthropic's "Code Execution with MCP" pattern, published in November 2025 and covered in depth in Class 20 of this tutorial. Instead of listing tools through MCP for the agent to choose from, you put a typed code SDK on a filesystem, and the agent writes and runs code that imports it. On a real workload that Anthropic measured, the pattern cut token use from 150,000 to 2,000.

If that pattern takes over, the search/introspect/execute tools in their current form may turn out to be a temporary design. The agent would then write code that imports a typed GraphQL SDK, and would not need to call discovery tools. The two patterns can also work together: a code-executing agent can still call MCP tools, and a tool-calling agent can still navigate a supergraph. What may change is which pattern most agents use.

For now, follow these two developments, and base today's decisions on what exists today. In 2026, production implementations run search/introspect/execute on Apollo MCP Server, Grafbase and IBM API Connect for GraphQL. Implementations in 2027 and 2028 may look different. Keep today's architecture easy to change. The patterns in this lesson and the next one meet that bar, because switching between them is mostly a config change.

Summary​

Dynamic discovery is the third operation-source mode. Instead of a curated list of tools, the agent gets search, introspect and execute, plus validate in Apollo's implementation, and writes operations at runtime against the part of the schema it found. The pattern gives up predictable cost and review before production, and in return it works on schemas too large to curate. That makes it right for enterprise supergraphs and the wrong default for moviedb. If your team adopts it, the policies around it are part of the feature: mutation and scope controls in the server config, and depth and complexity limits in the GraphQL layer.

Further Reading​

What's Next​

Class 5 covers Apollo MCP Server's four operational concerns in full, whichever operation-source mode you picked: custom scalars, auth propagation, per-operation descriptions, and deployment topology. It adds a fifth, Apollo Contracts for per-agent schema views, which matters most when running dynamic discovery on a federated graph. Class 5 also compares Apollo with the Spring AI MCP starter that Class 6 introduces. If you came here from Class 4 and dynamic discovery does not suit your stack, continue with Class 5 and the rest of Track A.