Skip to main content

Class 4: Apollo MCP Server in Twenty Minutes

Duration: 35 minutes | Difficulty: Intermediate | Prerequisites: Class 3.


Companion code

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

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

By the end of this lesson your existing moviedb Spring GraphQL service is exposed as a fully functioning server. Three named tools (RecommendMoviesForMood, SummarizeMovieReviews, AddToWatchlist) appear in any MCP client you point at the server, with typed inputs, descriptions taken from the schema, and JSON results. This lesson uses GraphQL and YAML files, and your Java code stays as it is. The whole exercise is one binary, one config file and three GraphQL operation files. The lesson is small on purpose, because Track A promises that a GraphQL endpoint plus the Apollo binary is enough for an MCP server. Class 5 then covers the four practical concerns this lesson leaves out, plus Apollo Contracts for per-agent schema views.

This lesson uses some terms that may be new. Each one has a short definition here, and a fuller explanation where the lesson puts it to work:

  • Apollo MCP Server is a standalone process from Apollo that serves GraphQL operations as MCP tools.
  • Operation file is a .graphql file that holds one named query or mutation. Apollo turns each one into one MCP tool.
  • JSON-RPC is the request-and-response message format, encoded as JSON, that MCP uses on the wire.
  • stdio and streamable HTTP are MCP's two transports. With stdio the client starts the server as a subprocess, and with streamable HTTP the server runs as an HTTP service.
  • SDL (Schema Definition Language) is the text form of a GraphQL schema, the syntax in schema.graphqls.
  • @oneOf is a built-in GraphQL directive that requires exactly one field of an input object to be set.
  • apollo-rs is the Rust GraphQL parser that Apollo MCP Server uses to read your schema.
  • Mutation mode is the Apollo setting overrides.mutation_mode, which decides whether mutation operations become tools.
  • MCP Inspector is the MCP project's developer tool for connecting to a server and calling its tools by hand.
  • General availability (GA) is the point at which a release counts as stable and fully supported.

What Apollo MCP Server Is​

Apollo MCP Server is a standalone process that takes a GraphQL endpoint and a directory of curated .graphql operation files and serves them as MCP tools. You run it as a separate binary next to your application. Apollo built it mainly for federated supergraphs, with Apollo Router behind it and an MCP client in front. It works equally well in front of a single Spring GraphQL service.

Think of Apollo MCP Server as a translator. MCP traffic comes in as over or streamable HTTP. Apollo then does four things:

  1. It finds the matching operation file.
  2. It fills in the arguments the MCP client supplied.
  3. It calls your GraphQL endpoint over plain HTTP.
  4. It converts the response into an MCP tool result.

Apollo sits between the model and your Spring service, so everything your GraphQL service already does still applies: auth, schema-level field authorization, query cost analysis and observability. Apollo adds a layer and leaves those in place.

This is the fastest path for some teams, and a bigger step for others. For teams that already run Apollo Router, adding MCP is a config change. For a team that runs a single Spring application, a separate Apollo process is one more service to operate. Class 5 looks at that decision in detail.

Installing the Binary​

Apollo MCP Server ships as a single binary. Apollo's install and run guide documents four ways to get it: an installer script, the release tarball, a container image, and the Rover CLI. This lesson uses the installer script. The others are described below so you can pick whichever fits how your team deploys.

The first route is the official Apollo MCP installer script. It downloads the release for your platform, unpacks it into the directory you are standing in, and finishes by printing You can now run the Apollo MCP Server using './apollo-mcp-server'. The leading ./ in that message matters. It tells you that the script worked only in the current directory, and left your PATH and your shell profile unchanged. You run the three commands after the installer yourself.

Those commands create the destination directory if it is missing, move the binary into it, and print the version. After that, apollo-mcp-server works from any directory. The -p on mkdir makes the line safe when /usr/local/bin already exists, as it does on most machines. With -p, mkdir succeeds and leaves the directory as it is, where a bare mkdir stops with File exists. On macOS, /usr/local/bin only exists if an installer has created it, so the mkdir line may be needed there:

curl -sSL https://mcp.apollo.dev/download/nix/latest | sh
sudo mkdir -p /usr/local/bin
sudo mv ./apollo-mcp-server /usr/local/bin/
apollo-mcp-server --version

To keep /usr/local/bin unchanged, run mkdir -p ~/.local/bin && mv ./apollo-mcp-server ~/.local/bin/ instead. That works if ~/.local/bin is already on your PATH. You can also leave the binary where the installer put it and run it as ./apollo-mcp-server, the form Apollo's documentation uses. That form only works in the directory that holds the binary. This lesson starts the server from the apollo/ directory, so the binary would have to be there. From here on, the instructions assume apollo-mcp-server is on your PATH. On Windows, the equivalent installer is iwr 'https://mcp.apollo.dev/download/win/latest' | iex.

In production you will want the version-pinned form of the same installer, which downloads exactly the release you name. The leading v is required, because the script uses the value as a literal path segment in the download URL:

curl -sSL https://mcp.apollo.dev/download/nix/v1.17.0 | sh

The second route is a direct binary download, for regulated environments that do not allow downloading and running shell installers. Download the release tarball for your platform from the Apollo MCP Server releases page. The assets are named apollo-mcp-server-v1.17.0-<target>.tar.gz, and aarch64-apple-darwin and x86_64-unknown-linux-gnu cover most laptops and servers. The archive unpacks into a dist/ directory, so the binary arrives one level down as dist/apollo-mcp-server:

tar -xzf apollo-mcp-server-v1.17.0-aarch64-apple-darwin.tar.gz
sudo mv dist/apollo-mcp-server /usr/local/bin/

The third route is the container image, ghcr.io/apollographql/apollo-mcp-server, tagged latest or v1.17.0. Apollo's documented form mounts the config file at /config.yaml and the project root at /data, which is the container's working directory, and passes the config path as the container command:

docker run --rm -p 8000:8000 \
-v $(pwd)/mcp-config.yaml:/config.yaml \
-v $(pwd):/data \
ghcr.io/apollographql/apollo-mcp-server:v1.17.0 /config.yaml

Following this lesson inside a container needs one change to the config written below, because localhost inside a container means the container itself. The endpoint has to become http://host.docker.internal:8080/graphql to reach the Spring service on your machine, and on Linux that name needs --add-host=host.docker.internal:host-gateway as well. Class 5 comes back to the image for deployment, where both processes run as containers.

The fourth route is the Rover CLI, worth knowing because Apollo's own quickstart is written around it. rover init --mcp scaffolds a project, and rover dev --mcp .apollo/mcp.local.yaml runs the MCP server alongside a local Apollo Router. That suits someone who starts from GraphOS with a supergraph to compose. Our GraphQL service already runs on :8080, and this lesson puts Apollo directly in front of it, so we skip Rover. Apollo's run guide documents the standalone binary as a fully supported option.

Whichever route you took, finish with apollo-mcp-server --version and confirm the binary reports a version number; the current release prints apollo-mcp-server 1.17.0. If your shell answers command not found, the binary is still in the directory where you ran the installer, and you still need to move it onto your PATH.

Do not install it from npm. The npm package called apollo-mcp-server is an unrelated Apollo.io CRM server from a different vendor. For this lesson, any version works that supports the streamable HTTP transport and the operations.source: local configuration shown below. Both have been standard since the 1.0 in October 2025. Version pinning matters more in production, which Class 5 covers.

Pin a known-good version in production

Pre-GA Apollo MCP Server releases changed the YAML schema several times, and the 1.x line has shipped two breaking changes of its own. 1.1.0 moved the default port from 5000 to 8000, and 1.5.0 removed the sse transport in favour of streamable_http. The config in this lesson works on both sides of those changes, because it sets its port and its transport explicitly. The same reasoning is why a deployment manifest should pin a specific version, and avoid latest. The current version at the time of writing is 1.17.0. It advertises MCP protocol revision 2025-11-25 by default, and negotiates 2026-07-28 with a client that asks for it. Check the releases page for whatever is current when you read this.

Writing Three Operation Files​

Apollo MCP Server's tools are the files in its operations directory. Each .graphql file becomes one MCP tool, and the GraphQL operation name becomes the tool name: the query RecommendMoviesForMood below surfaces as a tool called RecommendMoviesForMood, regardless of what the file is called. Name every operation, because Apollo skips an anonymous operation with a warning. This puts the persisted-operations idea from Class 1 into practice: each tool is a file.

Create an apollo/ directory in the root of your Spring project, and an operations/ directory inside it. Everything Apollo needs goes under apollo/, so the Spring project root holds only files the build uses. Every relative path in the config below resolves from apollo/, because that is the directory you will run the server from. Add three files: one for each of the two query operations Class 2 added, and one for the addToWatchlist mutation the Spring GraphQL tutorial already ships. Limit each selection set to the fields an agent needs. The agent sees only the fields you select, so you decide what it sees, one operation at a time.

apollo/operations/recommend_movies_for_mood.graphql
query RecommendMoviesForMood($mood: Mood!, $excludeWatched: Boolean = false) {
recommendMoviesForMood(mood: $mood, excludeWatched: $excludeWatched) {
id
title
releaseYear
genre
rating
}
}
apollo/operations/summarize_movie_reviews.graphql
query SummarizeMovieReviews($movieId: ID!) {
summarizeMovieReviews(movieId: $movieId) {
movieId
reviewCount
summary
themes
averageScore
}
}
apollo/operations/add_to_watchlist.graphql
mutation AddToWatchlist($subject: WatchlistSubjectInput!, $status: WatchStatus) {
addToWatchlist(subject: $subject, status: $status) {
id
status
content {
title
}
}
}

Three things about these files matter.

First, the selection set on recommendMoviesForMood leaves out directors and cast, even though Movie has those fields. A recommendation does not need them, and smaller responses cost fewer tokens and are easier for the model to read.

Second, AddToWatchlist declares $subject as a whole WatchlistSubjectInput! and passes it to the argument unchanged. It does not split it into separate $movieId and $tvShowId variables. So the agent supplies one object of that input type. The type's @oneOf constraint requires exactly one of movieId or tvShowId, and GraphQL itself enforces it on every call.

The selection set reads back only id, status, and the title through the Content interface. That is enough for the agent to confirm "added The Godfather to your list", without an inline fragment or the full Movie.

Third, the list stops at three tools on purpose. If you exposed every operation in the schema, Apollo would publish dozens of tools, which is almost the same as giving the agent the whole schema. A curated operations directory keeps the agent's tool list small enough for it to choose well. Class 7 reaches the same conclusion on Track B.

The operations directory is your tool registry

Treating the operations directory as a real project artifact has practical benefits:

  • You review it in pull requests like any other code change.
  • You can audit it: ls apollo/operations/ answers "what can the agent do?"
  • It works with the persisted-document tooling you may already use for frontend clients.
  • Because it is plain files, you can give different MCP servers different operation sets, so different agents see different tools.

The apollo-mcp-server Configuration​

Apollo MCP Server is configured by a YAML file. Create apollo/mcp-config.yaml, next to the operations directory:

apollo/mcp-config.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: local
paths:
- ./operations

overrides:
mutation_mode: explicit

The file has five sections, and each does one thing. endpoint tells Apollo where your GraphQL service is. For local development, that is the standard Spring for GraphQL endpoint at :8080/graphql. transport tells Apollo to expose its MCP surface over streamable HTTP at 127.0.0.1:8000. We use HTTP because the MCP server runs next to a Spring web service, and HTTP suits that setup. The stdio lesson, Class 19, covers the variant where Claude Desktop launches the server as a subprocess.

schema points Apollo at a local copy of your GraphQL . Apollo uses the schema for three things:

  • deriving tool descriptions from your schema's field descriptions
  • validating operations at startup
  • describing input types to the MCP client

The first means the descriptions we wrote in Class 3 now also document the tools. is better than copying. The link always reads the current schema, and a copy falls out of date as soon as you edit the schema and forget to copy it again:

cd apollo
ln -s ../src/main/resources/graphql/schema.graphqls schema.graphqls

Two other sources exist. schema.source: uplink pulls the supergraph schema from GraphOS for federated deployments. schema.source: graphos, new in 1.17.0, fetches the latest published schema of a non-federated graph, and picks up republished schemas without a restart. For our local single-service case, local is simplest.

operations.source: local with paths: ["./operations"] tells Apollo to scan the operations directory for .graphql files and treat each one as a tool. To add a fourth tool later, add a file such as apollo/operations/foo.graphql.

AddToWatchlist only exists if you set overrides.mutation_mode: explicit. Apollo MCP Server defaults to mutation_mode: none, which refuses mutation operation files. The server still starts cleanly and registers the two queries. It logs one warning, "Skipping mutation operation AddToWatchlist", which is easy to miss in the startup output, and the tool is missing from tools/list. The default is a deliberate safety choice: an agent gets write access only when a person turns it on. explicit allows the mutations you wrote as operation files. The third value, all, also allows mutations in the dynamic-discovery mode that Class 4b covers, which grants much more.

One Line the Schema Needs Before Apollo Will Read It​

If you start the server against the schema as it is now, it fails at startup:

Error: Could not parse GraphQL schema: Error: cannot find directive `@oneOf` in this document
╭─[ schema.graphql:515:29 ]
│
515 │ input WatchlistSubjectInput @oneOf {
│ ───┬──
│ ╰──── directive not defined
─────╯

The schema is valid, and both parsers are correct. They disagree because they check different things.

@oneOf became a built-in directive in the September 2025 edition of the GraphQL specification. graphql-java, which your Spring service runs, knows the built-in directives and adds their definitions to every schema it builds. So schema.graphqls could leave @oneOf undeclared, and the Spring app has started without the declaration. Apollo MCP Server parses the SDL with apollo-rs, which validates the file as a self-contained document. Every directive an input type uses has to be defined somewhere in that file. apollo-rs does not add any definitions itself, so @oneOf is an undefined name and parsing fails.

Declaring a built-in directive explicitly is legal, and it satisfies both parsers, so the fix is one line next to the validation directives already at the top of the file:

src/main/resources/graphql/schema.graphqls (one new line at the top of the file)
directive @oneOf on INPUT_OBJECT

Run ./mvnw clean test again to confirm that graphql-java still accepts the schema. It does, because a redeclared built-in directive matches the definition graphql-java would have added itself.

This particular problem goes away once apollo-rs supports the 2025 edition. The general point stays: when a different GraphQL engine reads a schema that has run for months, expect at least one thing the original engine was supplying silently. This is the first real cost of the extra process that Class 1 described in general terms.

Running and Inspecting​

Running the full stack takes three terminals.

In the first terminal, the moviedb Spring service:

cd ~/projects/moviedb
./mvnw spring-boot:run

In the second terminal, Apollo MCP Server, started from the apollo/ directory so that the relative paths in the config resolve:

cd ~/projects/moviedb/apollo
apollo-mcp-server mcp-config.yaml

The config file path is a positional argument; the server does not accept a --config flag.

Startup logs one line per operation file it loaded and one for the listener:

INFO Apollo MCP Server v1.17.0 // (c) Apollo Graph, Inc. // Licensed under MIT
INFO load_tool: Tool AddToWatchlist loaded with a character count of 2696. Estimated tokens: 674
INFO load_tool: Tool RecommendMoviesForMood loaded with a character count of 5798. Estimated tokens: 1449
INFO load_tool: Tool SummarizeMovieReviews loaded with a character count of 2061. Estimated tokens: 515
INFO Starting MCP server in Streamable HTTP mode port=8000 address=127.0.0.1

Look at the token estimates, because they show what the tool list costs. The model reads those tokens on every agent turn, before it reads any answer. RecommendMoviesForMood costs more than twice as much as each of the other two, because of the descriptions on the Mood enum from Class 3. At three tools, that cost is acceptable. Class 4b returns to this, because it is the main reason large graphs use dynamic discovery instead of a curated directory.

If startup fails, there are three likely causes:

  • The endpoint URL does not match. Spring runs on :8080 by default.
  • schema.graphqls is missing, out of date, or lacks the @oneOf declaration from the previous section.
  • An operation file has a typo. Apollo validates operation files against the schema at startup, so these mistakes show up before an agent connects.

In the third terminal, the official MCP Inspector:

npx @modelcontextprotocol/[email protected]

The command prints a URL with an API token in the query string, and opens your browser at that URL. Keep the terminal open. If you close the tab, http://127.0.0.1:6274 opens Inspector again, because Inspector puts the token into the page it serves.

Connecting takes a few clicks, and every later lesson uses the same steps. The labels below are from Inspector 2.6.0, and older walkthroughs online show a different layout.

  1. Inspector opens on a Servers list already holding three example servers it ships with, all disconnected. Ignore those.

  2. Choose Add Servers, then + Add manually.

  3. Fill in Server ID, which becomes the key in Inspector's ~/.mcp-inspector/mcp.json:

    moviedb-apollo
  4. Change Transport from its stdio (local process) default to streamable-http. The command fields are replaced by a single URL field.

  5. Fill in that URL. The /mcp path matters, because the server exposes its MCP endpoint there instead of at the bare port:

    http://127.0.0.1:8000/mcp
  6. Press Add. The new row appears reading Disconnected.

  7. Flip the toggle on that row. It changes to Connected with a handshake time, and a Servers / Tools switch appears in the header next to the server's name.

You use that header switch for everything below, and it appears only while a server is connected. Click Tools on it. The panel lists the three operations with the descriptions Apollo derived from your schema, and selecting one opens its input pane beside the list.

Select RecommendMoviesForMood and fill in mood. Inspector shows a free-text box for it where you might expect a dropdown, and it parses what you type as JSON, so the value needs its quotes:

"COMFORT"

If you type COMFORT without quotes, the box turns red with Not valid JSON, and Execute Tool stays disabled until the value is valid JSON. The cause is in Apollo. Apollo describes the argument as "mood": {"$ref": "#/definitions/Mood"} and does not list the enum values inline. Inspector builds a picker only for inline enums, so it shows a JSON editor. Press Execute Tool, and real movie data comes back as JSON. Then open AddToWatchlist and read its input pane before you run anything. subject appears as an object in which movieId and tvShowId are both optional. status is optional because the server defaults it to WANT_TO_WATCH.

The schema's @oneOf exactly-one rule is not carried into the generated , because Apollo records only field types and non-null requirements when it translates input types. GraphQL validation in the Spring service enforces the rule when the tool runs, and the input pane cannot show it. That is one reason the tool descriptions in Class 5 say "provide exactly one of movieId or tvShowId" in words the model can read. Run the tool with subject set to:

{"movieId": "1"}

The call returns an unauthorized error. The reason shows what putting Apollo in front of your service changed, and what it kept. The base app guards addToWatchlist with @PreAuthorize("isAuthenticated()"), and this lesson's config does not forward an Authorization header to Spring. The request reaches the resolver without an authenticated caller, and Spring Security rejects it before any watch-list code runs. Class 5 wires a real token through, and the same call then succeeds. At this point the three operations work as tools with typed inputs, and the same rules as before MCP protect their writes.

Summary​

Three movie operations are now MCP tools. The Spring code and the security model stayed the same, and the schema gained one directive declaration. A new process now sits between your Spring app and the agent, and it translates between the two protocols. From now on, you add a tool by adding an operation file, and remove one by deleting the file. Decisions about what an agent may do now happen when you review the operations directory. Reviewing a small set of operation files is quick, and a change is easy to undo.

This is what Track A gives you: the smallest possible change to your existing system to put MCP in front of it. For a team running a single Spring service the cost is one extra process and a YAML file. For a federated team that already runs Apollo Router, the cost is smaller still, because the team already operates a similar Apollo binary.

Further Reading​

What's Next​

The lesson title promised twenty minutes, and the setup does take about that long. It also left four practical concerns open.

What happens when your schema uses custom scalars Apollo does not natively understand? How do you propagate the agent's user identity from MCP through Apollo to your Spring security model so that field-level authorization keeps working? How do you write tool descriptions that are different from your schema descriptions (for example, when the schema description targets frontend developers and the agent needs different framing)? And where does Apollo MCP Server actually live in production: same host, sidecar, separate service? Class 5 names the four problems and walks through the answers, and adds Apollo Contracts for per-agent schema views. It then assesses when Apollo is the right tool and when the Spring AI MCP starter or the DIY in-Spring approach fits better.