This is page 1 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
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
```
1 | use flake
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | .idea/*
2 |
3 | # Let run configurations be unignored so they can be shared if desired
4 | !.idea/runConfigurations/
5 |
6 | .DS_Store
7 |
8 | # Generated by Cargo
9 | # will have compiled files and executables
10 | debug/
11 | target/
12 |
13 | # These are backup files generated by rustfmt
14 | **/*.rs.bk
15 |
16 | # MSVC Windows builds of rustc generate these, which store debugging information
17 | *.pdb
18 |
19 | # RustRover
20 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
21 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
22 | # and can be added to the global gitignore or merged into this file. For a more nuclear
23 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
24 | #.idea/
25 |
26 | # Generated by direnv
27 | .direnv/
28 |
29 | # Symlink created by nix
30 | result
31 |
```
--------------------------------------------------------------------------------
/nix/mcp-server-tools/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Server Tools
2 |
3 | This directory contains autogenerated nix evaluations for the various MCP server
4 | tools for use during development.
5 |
6 | ## Adding More Servers
7 |
8 | In order to add more server binaries to this autogenerated evaluation, do the
9 | following:
10 |
11 | 1. Add the tool to the list of tools in `mcp-servers.json`
12 |
13 | ```json
14 | {
15 | <EXISTING TOOLING>,
16 | "@modelcontextprotocol/server-<NAME OF SERVER>"
17 | }
18 | ```
19 |
20 | 2. Regenerate the autogenerated files by running the following in the current
21 | directory
22 |
23 | ```shell
24 | $ node2nix -i mcp-servers.json --pkg-name nodejs_22 \
25 | -o node-generated/node-packages.nix \
26 | -c node-generated/default.nix \
27 | -e node-generated/node-env.nix
28 | ```
29 |
30 | 3. Reload your nix development shell
31 |
```
--------------------------------------------------------------------------------
/.changesets/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Upcoming Changelog Entries
2 |
3 | This directory keeps files which individually represent entries that will represent the CHANGELOG produced for the next release.
4 |
5 | > **Note**
6 | >
7 | > The files within this directory use a **convention which must be obeyed** in order for the file to be slurped up by automated tooling.
8 |
9 | > **Warning**
10 | >
11 | > The aforementioned **tooling doesn't exist yet** but will be created soon. 😺
12 |
13 | ### How to create a Changelog entry
14 |
15 | 1. Push the change you are writing a changeset for up to GitHub.
16 | 2. Open a pull request for it. Note that your PR title and body will be used to pre-populate the changeset.
17 | 3. On your local checkout, **run `cargo xtask changeset create` from the root of the repository** and follow the prompts.
18 | 4. Add, commit and push the changeset file that is created and push it up to GitHub.
19 |
20 | ### Conventions used in this `.changesets/` directory
21 |
22 | The convention used in this directory and obeyed by the `cargo xtask changeset create` command is:
23 |
24 | 1. Files in this directory must use the `.md` file extension.
25 | 2. There must not be multiple changelog entries in a single file.
26 | 3. Files *must start with a prefix* that indicates the classification of the changeset. The prefixes are as follows:
27 | - **Breaking**: `breaking_`
28 | - **Feature**: `feat_`
29 | - **Fixes**: `fix_`
30 | - **Configuration**: `config_`
31 | - **Maintenance**: `maint_`
32 | - **Documentation**: `docs_`
33 | - **Experimental**: `exp_`
34 | 4. The pattern following the prefix can be anything that matches `[a-z_]+` (i.e., any number of lowercased `a-z` and `_`). Again, `.md` must be on the end as the extension. For example, `feat_flying_forest_foxes.md`.
35 | 5. Other files not matching the above convention will be ignored, including this `README.md`.
36 | 6. The files must use the following format:
37 |
38 | ### Brief but complete sentence that stands on its own - @USERNAME PR #PULL_NUMBER
39 |
40 | A description of the fix which stands on its own separate from the title. It should embrace the use of Markdown to stylize the commentary so it looks great on the GitHub Releases, when shared on social cards, etc.
41 |
42 | Note the key components:
43 |
44 | - A _brief but complete_ sentence as a **title** that stands on its own without needing to read the description
45 | - A GitHub reference to **one or more authors** who contributed
46 | - A GitHub reference to the **pull request**
47 | - A **description** which _doesn't need the title's context_ to be be understood
48 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | <div align="center">
2 | <a href="https://www.apollographql.com/"><img src="https://raw.githubusercontent.com/apollographql/apollo-client-devtools/main/assets/apollo-wordmark.svg" height="100" alt="Apollo Client"></a>
3 | </div>
4 |
5 | 
6 | 
7 | 
8 | 
9 | 
10 | [](https://codecov.io/github/apollographql/apollo-mcp-server)
11 |
12 | # Apollo MCP Server
13 |
14 | Apollo MCP Server is a [Model Context Protocol](https://modelcontextprotocol.io/) server that exposes GraphQL operations as MCP tools. It provides a standard way for AI models to access and orchestrate your APIs running with Apollo.
15 |
16 | ## Documentation
17 |
18 | See [the documentation](https://www.apollographql.com/docs/apollo-mcp-server/) for full details. This README shows the basics of getting this MCP server running. More details are available on the documentation site.
19 |
20 | ## Installation
21 |
22 | You can either build this server from source, if you have Rust installed on your workstation, or you can follow the [installation guide](https://www.apollographql.com/docs/apollo-mcp-server/run). To build from source, run `cargo build` from the root of this repository and the server will be built in the `target/debug` directory.
23 |
24 | ## Getting started
25 |
26 | Follow the [quickstart tutorial](https://www.apollographql.com/docs/apollo-mcp-server/quickstart) to get started with this server.
27 |
28 | ## Usage
29 |
30 | Full usage of Apollo MCP Server is documented on the [user guide](https://www.apollographql.com/docs/apollo-mcp-server/run). There are a few items that are necessary for this server to function. Specifically, the following things must be configured:
31 |
32 | 1. A graph for the MCP server to sit in front of.
33 | 2. Definitions for the GraphQL operations that should be exposed as MCP tools.
34 | 3. A configuration file describing how the MCP server should run.
35 | 4. A connection to an MCP client, such as an LLM or [MCP inspector](https://modelcontextprotocol.io/legacy/tools/inspector).
36 |
37 | These are all described on the user guide. Specific configuration options for the configuration file are documented in the [config file reference](https://www.apollographql.com/docs/apollo-mcp-server/config-file).
38 |
39 | ## Contributions
40 |
41 | Checkout the [contributor guidelines](https://github.com/apollographql/apollo-mcp-server/blob/main/CONTRIBUTING.md) for more information.
42 |
43 | ## Licensing
44 |
45 | This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for the full license text.
46 |
47 | # Security
48 |
49 | Refer to our [security policy](https://github.com/apollographql/.github/blob/main/SECURITY.md).
50 |
51 | > [!IMPORTANT]
52 | > **Do not open up a GitHub issue if a found bug is a security vulnerability**, and instead to refer to our [security policy](https://github.com/apollographql/.github/blob/main/SECURITY.md).
53 |
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # The Space Devs MCP Server
2 |
3 | This folder contains an example usage of the Apollo MCP server for [The Space Devs](https://thespacedevs.com/) APIs, a set of APIs that exposes spaceflight information. We have a [hosted GraphQL endpoint](https://thespacedevs-production.up.railway.app/) that exposes The Space Devs Launch Library v2 REST APIs using Apollo Connectors.
4 |
5 | ## Setup
6 |
7 | To use this example, you must setup on of these three options to run the Apollo MCP server locally:
8 |
9 | 1. **_(Coming Soon)_** Use `rover dev` to run the Apollo MCP server - requires [installing `rover`](https://www.apollographql.com/docs/rover/getting-started)
10 | 2. Run the Docker image - requires having [Docker installed](https://docs.docker.com/engine/install/)
11 | 3. Build the `apollo-mcp-server` repo from source
12 |
13 | ```bash
14 | git clone https://github.com/apollographql/apollo-mcp-server
15 | cd apollo-mcp-server
16 | cargo build
17 |
18 | # Built binaries will be located in ./target/debug/apollo-mcp-server
19 | ```
20 |
21 | If you don't have an MCP client you plan on using right away, you can inspect the tools of the Apollo MCP server using the MCP Inspector:
22 |
23 | ```sh
24 | npx @modelcontextprotocol/inspector
25 | ```
26 |
27 | ## Using STDIO and invoking Apollo MCP server with command
28 |
29 | This option is typically used when you have built the source repository and use the binary outputs in the `target/build/*` folder.
30 |
31 | There are operations located at `./operations/*.graphql` for you to use in your configuration. You can provide a set of operations in your MCP configuration along with the `--introspection` option that enables the LLM to generate a dynamic operation along with the ability to execute it.
32 |
33 | Here is an example configuration you can use _(Note: you must provide your fill path to the binary in the command. Make sure to replace the command with the path to where you cloned the repository)_:
34 |
35 | ```json
36 | {
37 | "mcpServers": {
38 | "thespacedevs": {
39 | "command": "/Users/michaelwatson/Documents/GitHub/apollographql/apollo-mcp-server/target/debug/apollo-mcp-server",
40 | "args": [
41 | "graphql/TheSpaceDevs/config.yaml"
42 | ]
43 | }
44 | }
45 | }
46 | ```
47 |
48 | ## Using Streamable HTTP with Apollo MCP server
49 |
50 | There are operations located at `./operations/*.graphql` for you to use in your configuration. You can provide a set of operations in your MCP configuration that enables the LLM to generate a dynamic operation along with the ability to execute it.
51 |
52 | ### Running with `rover dev`
53 |
54 | ```BASH
55 | rover dev --supergraph-config supergraph.yaml --mcp config.yaml
56 | ```
57 |
58 | ### Running Apollo MCP server Docker image
59 |
60 | 1. Start up the MCP server locally
61 |
62 | ```bash
63 | docker run \
64 | -it --rm \
65 | --name apollo-mcp-server \
66 | -p 8000:8000 \
67 | -v $PWD/graphql/TheSpaceDevs/config.yaml:/config.yaml \
68 | -v $PWD/graphql/TheSpaceDevs:/data \
69 | ghcr.io/apollographql/apollo-mcp-server:latest /config.yaml
70 | ```
71 |
72 | 2. Add the MCP port to your MCP Server configuration for the client application you are running. If you are running locally, the server link will be `http://127.0.0.1:8000/mcp`.
73 |
74 | _Note: Claude Desktop currently doesn't support SSE_
75 |
76 | ```
77 | {
78 | "mcpServers": {
79 | "thespacedevs": {
80 | "command": "npx",
81 | "args": [
82 | "mcp-remote",
83 | "http://127.0.0.1:8000/mcp"
84 | ]
85 | }
86 | }
87 | }
88 | ```
89 |
90 | ### Running binary built from source code
91 |
92 | Here is an example configuration you can use _(Note: you must provide your fill path to the binary in the command. Make sure to replace the command with the path to where you cloned the repository)_:
93 |
94 | ```json
95 | {
96 | "mcpServers": {
97 | "thespacedevs": {
98 | "command": "/Users/michaelwatson/Documents/GitHub/apollographql/apollo-mcp-server/target/debug/apollo-mcp-server",
99 | "args": [
100 | "graphql/TheSpaceDevs/config.yaml"
101 | ]
102 | }
103 | }
104 | }
105 | ```
106 |
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | 
2 | 
3 | 
4 | 
5 | 
6 |
7 | ## How to contribute to Apollo MCP Server
8 |
9 | ### Bug Reporting
10 |
11 | > [!WARNING]
12 | > **Do not open up a GitHub issue if the bug is a security vulnerability**, and instead refer to our [security policy](https://github.com/apollographql/.github/blob/main/SECURITY.md).
13 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/apollographql/apollo-mcp-server/issues) as well as the [Apollo Community forums](https://community.apollographql.com/latest).
14 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/apollographql/apollo-mcp-server/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
15 | * If appropriate, add the most relevant label but leave empty if unsure.
16 |
17 | ### Did you write a patch that fixes a bug?
18 |
19 | * Refer to the simple [branching guide](#branching-strategy) for the project.
20 | * Open a new GitHub pull request with the patch.
21 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
22 | * Before submitting, please read the [branching strategy](#branching-strategy) and [code review guidelines](#code-review-guidelines) to learn more about our coding conventions, branching strategies, code reviews guidelines, etc.
23 |
24 | ### Do you intend to add a new feature or change an existing one?
25 |
26 | * Suggest your change as a new [issue](https://github.com/apollographql/apollo-mcp-server/issues) using the `enhancement` label.
27 | * You can also suggest changes and features using the [Apollo Community forums](https://community.apollographql.com/latest).
28 | * Once the feature is coded and complete, open a GitHub pull request providing clear description of the feature/change and include any relevant links to discussions.
29 | * Before submitting, please read the [branching strategy](#branching-strategy) and [code review guidelines](#code-review-guidelines) to learn more about our coding conventions, branching strategies, code reviews guidelines, etc.
30 |
31 | ### Do you have questions about the code or about Apollo MCP Server itself?
32 |
33 | * Ask any question about Apollo MCP Server using either the [issues](https://github.com/apollographql/apollo-mcp-server/issues) page or the [Apollo Community forums](https://community.apollographql.com/latest).
34 | * If using the issues page, please use the `question` label.
35 |
36 | Thanks!
37 |
38 | Apollo MCP Server team
39 |
40 | ---
41 |
42 | ### Code of Conduct
43 |
44 | Please refer to our [code of conduct policy](https://github.com/apollographql/router/blob/dev/CONTRIBUTING.md#code-of-conduct).
45 |
46 | ---
47 |
48 | ### Branching strategy
49 | The Apollo MCP Server project follows a pseudo [GitFlow](https://docs.aws.amazon.com/prescriptive-guidance/latest/choosing-git-branch-approach/gitflow-branching-strategy.html) branch strategy.
50 |
51 | 1. All feature/bug fix/patch work should branch off the `develop` branch.
52 |
53 | ### Code review guidelines
54 | It’s important that every piece of code in Apollo packages is reviewed by at least one core contributor familiar with that codebase. Here are some things we look for:
55 |
56 | 1. Required CI checks pass. This is a prerequisite for the review, and it is the PR author's responsibility. As long as the tests don’t pass, the PR won't get reviewed.
57 | 2. Simplicity. Is this the simplest way to achieve the intended goal? If there are too many files, redundant functions, or complex lines of code, suggest a simpler way to do the same thing. In particular, avoid implementing an overly general solution when a simple, small, and pragmatic fix will do.
58 | 3. Testing. Please make sure that the tests ensure that the code won’t break when other stuff change around it. The error messages in the test should help identify what is broken exactly and how. The tests should test every edge case if possible. Please make sure you get as much coverage as possible.
59 | 4. No unnecessary or unrelated changes. PRs shouldn’t come with random formatting changes, especially in unrelated parts of the code. If there is some refactoring that needs to be done, it should be in a separate PR from a bug fix or feature, if possible.
60 | 5. Please run `cargo test`, `cargo clippy`, and `cargo fmt` prior to creating a PR.
61 |
62 | ### Code Coverage
63 |
64 | Apollo MCP Server uses comprehensive code coverage reporting to ensure code quality and test effectiveness.
65 | The project uses [cargo-llvm-cov](https://crates.io/crates/cargo-llvm-cov) for generating code coverage reports and [Codecov](https://www.codecov.io/) for coverage analysis and reporting. Coverage is automatically generated and reported on every pull request through GitHub Actions.
66 |
67 | #### Coverage Targets
68 |
69 | The project maintains the following coverage targets, configured in `codecov.yml`:
70 |
71 | - **Project Coverage**: Automatically maintained - should increase overall coverage on each PR
72 | - **Patch Coverage**: 80% - requires 80% coverage on all new/modified code
73 |
74 | These targets help ensure that:
75 |
76 | - The overall codebase coverage doesn't decrease over time
77 | - New code is well-tested before being merged
78 |
```
--------------------------------------------------------------------------------
/apollo.config.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "rover": {
3 | }
4 | }
```
--------------------------------------------------------------------------------
/xtask/src/commands/mod.rs:
--------------------------------------------------------------------------------
```rust
1 | pub(crate) mod changeset;
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/platform_api/operation_collections.rs:
--------------------------------------------------------------------------------
```rust
1 | pub mod collection_poller;
2 | pub mod error;
3 | pub mod event;
4 |
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/.vscode/extensions.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "recommendations": ["apollographql.vscode-apollo"]
3 | }
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/lib.rs:
--------------------------------------------------------------------------------
```rust
1 | pub mod files;
2 | pub(crate) mod logging;
3 | pub mod platform_api;
4 | pub mod uplink;
5 |
```
--------------------------------------------------------------------------------
/.cargo/config.toml:
--------------------------------------------------------------------------------
```toml
1 | [alias]
2 | xtask = "run --locked --package xtask --manifest-path xtask/Cargo.toml --"
3 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/introspection.rs:
--------------------------------------------------------------------------------
```rust
1 | //! Allow an AI agent to introspect a GraphQL schema.
2 |
3 | mod minify;
4 | pub(crate) mod tools;
5 |
```
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
```toml
1 | [toolchain]
2 | channel = "1.90.0"
3 | profile = "default"
4 | components = ["rust-analyzer", "rust-src"]
5 |
```
--------------------------------------------------------------------------------
/graphql/weather/operations/forecast.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query GetForecast($coordinate: InputCoordinate!) {
2 | forecast(coordinate: $coordinate) {
3 | detailed
4 | }
5 | }
6 |
```
--------------------------------------------------------------------------------
/graphql/weather/operations/alerts.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query GetAlerts($state: String!) {
2 | alerts(state: $state) {
3 | severity
4 | description
5 | instruction
6 | }
7 | }
8 |
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/persisted_queries.config.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "documents": [
3 | "operations/**/*.{graphql,gql,js,jsx,ts,tsx}"
4 | ],
5 | "output": "persisted_queries/apollo.json"
6 | }
```
--------------------------------------------------------------------------------
/clippy.toml:
--------------------------------------------------------------------------------
```toml
1 | allow-expect-in-tests = true
2 | allow-panic-in-tests = true
3 | allow-unwrap-in-tests = true
4 | allow-indexing-slicing-in-tests = true
5 |
```
--------------------------------------------------------------------------------
/nix/mcp-server-tools/node-mcp-servers.json:
--------------------------------------------------------------------------------
```json
1 | [
2 | "@modelcontextprotocol/inspector",
3 | "@modelcontextprotocol/server-filesystem",
4 | "@modelcontextprotocol/server-memory"
5 | ]
6 |
```
--------------------------------------------------------------------------------
/graphql/weather/supergraph.yaml:
--------------------------------------------------------------------------------
```yaml
1 | federation_version: =2.10.0
2 | subgraphs:
3 | weather:
4 | routing_url: http://localhost # this value is ignored
5 | schema:
6 | file: weather.graphql
7 |
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/server-config.template.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "mcp-server": {
4 | "command": "../../target/release/apollo-mcp-server",
5 | "args": ["./<test-dir>/config.yaml"]
6 | }
7 | }
8 | }
9 |
```
--------------------------------------------------------------------------------
/xtask/src/commands/changeset/scalars.rs:
--------------------------------------------------------------------------------
```rust
1 | /// The GitHub API uses a Scalar called URI. I promise it's still
2 | /// just a String.
3 | #[allow(clippy::upper_case_acronyms)]
4 | pub(crate) type URI = String;
5 |
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/supergraph.yaml:
--------------------------------------------------------------------------------
```yaml
1 | federation_version: =2.10.0
2 | subgraphs:
3 | thespacedevs:
4 | routing_url: https://thespacedevs-production.up.railway.app/
5 | schema:
6 | file: api.graphql
7 |
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/apollo.config.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "client": {
3 | "includes": ["./operations/**/*.graphql"],
4 | "service": {
5 | "name": "TheSpaceDevs",
6 | "localSchemaFile": "./api.graphql"
7 | }
8 | }
9 | }
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/meter.rs:
--------------------------------------------------------------------------------
```rust
1 | use opentelemetry::{global, metrics::Meter};
2 | use std::sync::LazyLock;
3 |
4 | pub static METER: LazyLock<Meter> = LazyLock::new(|| global::meter(env!("CARGO_PKG_NAME")));
5 |
```
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "recommendations": [
3 | "mkhl.direnv",
4 | "vadimcn.vscode-lldb",
5 | "streetsidesoftware.code-spell-checker",
6 | "apollographql.vscode-apollo"
7 | ]
8 | }
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/introspection/tools.rs:
--------------------------------------------------------------------------------
```rust
1 | //! MCP tools to allow an AI agent to introspect a GraphQL schema and execute operations.
2 |
3 | pub(crate) mod execute;
4 | pub(crate) mod introspect;
5 | pub(crate) mod search;
6 | pub(crate) mod validate;
7 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/platform_api/operation_collections/event.rs:
--------------------------------------------------------------------------------
```rust
1 | use super::collection_poller::OperationData;
2 | use super::error::CollectionError;
3 |
4 | pub enum CollectionEvent {
5 | UpdateOperationCollection(Vec<OperationData>),
6 | CollectionError(CollectionError),
7 | }
8 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/logging/defaults.rs:
--------------------------------------------------------------------------------
```rust
1 | use super::LogRotationKind;
2 | use tracing::Level;
3 |
4 | pub(super) const fn log_level() -> Level {
5 | Level::INFO
6 | }
7 |
8 | pub(super) const fn default_rotation() -> LogRotationKind {
9 | LogRotationKind::Hourly
10 | }
11 |
```
--------------------------------------------------------------------------------
/graphql/weather/operations/all.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query GetAllWeatherData($coordinate: InputCoordinate!, $state: String!) {
2 | forecast(coordinate: $coordinate) {
3 | detailed
4 | }
5 | alerts(state: $state) {
6 | severity
7 | description
8 | instruction
9 | }
10 | }
11 |
```
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
```yaml
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | # Should increase overall coverage on each PR
6 | target: auto
7 | patch:
8 | default:
9 | # Require 80% coverage on all new/modified code
10 | target: 80%
11 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/schemas.rs:
--------------------------------------------------------------------------------
```rust
1 | use std::collections::HashMap;
2 |
3 | use schemars::JsonSchema;
4 |
5 | pub(super) fn header_map(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
6 | // A header map is just a hash map of string to string with extra validation
7 | HashMap::<String, String>::json_schema(generator)
8 | }
9 |
```
--------------------------------------------------------------------------------
/graphql/weather/config.yaml:
--------------------------------------------------------------------------------
```yaml
1 | transport:
2 | type: streamable_http
3 | operations:
4 | source: local
5 | paths:
6 | - ./graphql/weather/operations
7 | schema:
8 | source: local
9 | path: ./graphql/weather/api.graphql
10 | introspection:
11 | execute:
12 | enabled: true
13 | introspect:
14 | enabled: true
15 | search:
16 | enabled: true
17 | validate:
18 | enabled: true
19 | cors:
20 | enabled: true
21 | allow_any_origin: true
22 |
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/pq-manifest/config.yaml:
--------------------------------------------------------------------------------
```yaml
1 | endpoint: https://thespacedevs-production.up.railway.app/
2 | transport:
3 | type: stdio
4 | operations:
5 | source: manifest
6 | path: ./pq-manifest/apollo.json
7 | schema:
8 | source: local
9 | path: ./pq-manifest/api.graphql
10 | overrides:
11 | mutation_mode: all
12 | introspection:
13 | execute:
14 | enabled: true
15 | introspect:
16 | enabled: true
17 | search:
18 | enabled: true
19 | validate:
20 | enabled: true
21 |
```
--------------------------------------------------------------------------------
/crates/apollo-schema-index/src/error.rs:
--------------------------------------------------------------------------------
```rust
1 | use tantivy::TantivyError;
2 |
3 | /// An error during indexing
4 | #[derive(Debug, thiserror::Error)]
5 | pub enum IndexingError {
6 | #[error("Unable to index schema: {0}")]
7 | TantivyError(#[from] TantivyError),
8 | }
9 |
10 | /// An error in a search operation
11 | #[derive(Debug, thiserror::Error)]
12 | pub enum SearchError {
13 | #[error("Search error: {0}")]
14 | TantivyError(#[from] TantivyError),
15 | }
16 |
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/local-operations/operations/GetAstronautsCurrentlyInSpace.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query GetAstronautsCurrentlyInSpace {
2 | astronauts(filters: { inSpace: true, search: "" }) {
3 | results {
4 | id
5 | name
6 | timeInSpace
7 | lastFlight
8 | agency {
9 | name
10 | abbrev
11 | country {
12 | name
13 | }
14 | }
15 | nationality {
16 | name
17 | nationalityName
18 | }
19 | image {
20 | thumbnail
21 | }
22 | }
23 | }
24 | }
25 |
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/operations/GetAstronautsCurrentlyInSpace.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query GetAstronautsCurrentlyInSpace {
2 | astronauts(filters: { inSpace: true, search: "" }) {
3 | results {
4 | id
5 | name
6 | timeInSpace
7 | lastFlight
8 | agency {
9 | name
10 | abbrev
11 | country {
12 | name
13 | }
14 | }
15 | nationality {
16 | name
17 | nationalityName
18 | }
19 | image {
20 | thumbnail
21 | }
22 | }
23 | }
24 | }
25 |
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/local-operations/config.yaml:
--------------------------------------------------------------------------------
```yaml
1 | endpoint: https://thespacedevs-production.up.railway.app/
2 | transport:
3 | type: stdio
4 | operations:
5 | source: local
6 | paths:
7 | - ./local-operations/operations
8 | schema:
9 | source: local
10 | path: ./local-operations/api.graphql
11 | overrides:
12 | mutation_mode: all
13 | introspection:
14 | execute:
15 | enabled: true
16 | introspect:
17 | enabled: true
18 | search:
19 | enabled: true
20 | validate:
21 | enabled: true
22 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/operations.rs:
--------------------------------------------------------------------------------
```rust
1 | //! Operations
2 | //!
3 | //! This module includes transformation utilities that convert GraphQL operations
4 | //! into MCP tools.
5 |
6 | mod mutation_mode;
7 | mod operation;
8 | mod operation_source;
9 | mod raw_operation;
10 | mod schema_walker;
11 |
12 | pub use mutation_mode::MutationMode;
13 | pub use operation::{Operation, operation_defs, operation_name};
14 | pub use operation_source::OperationSource;
15 | pub use raw_operation::RawOperation;
16 |
```
--------------------------------------------------------------------------------
/docs/source/licensing.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Apollo MCP Server Licensing
3 | ---
4 |
5 | Source code for Apollo MCP Server in GitHub is covered by the MIT License. All files in the Apollo MCP Server repository are licensed under MIT, unless explicitly stated otherwise in a file header or license file in a subdirectory.
6 |
7 | See the repository [LICENSE](https://github.com/apollographql/apollo-mcp-server/blob/main/LICENSE) for the full license text.
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/operations/mutation_mode.rs:
--------------------------------------------------------------------------------
```rust
1 | use schemars::JsonSchema;
2 | use serde::{Deserialize, Serialize};
3 |
4 | #[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Copy, JsonSchema)]
5 | #[serde(rename_all = "snake_case")]
6 | pub enum MutationMode {
7 | /// Don't allow any mutations
8 | #[default]
9 | None,
10 | /// Allow explicit mutations, but don't allow the LLM to build them
11 | Explicit,
12 | /// Allow the LLM to build mutations
13 | All,
14 | }
15 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/schema_source.rs:
--------------------------------------------------------------------------------
```rust
1 | use std::path::PathBuf;
2 |
3 | use schemars::JsonSchema;
4 | use serde::Deserialize;
5 |
6 | /// Source for upstream GraphQL schema
7 | #[derive(Debug, Default, Deserialize, JsonSchema)]
8 | #[serde(tag = "source", rename_all = "snake_case")]
9 | pub enum SchemaSource {
10 | /// Schema should be loaded (and watched) from a local file path
11 | Local { path: PathBuf },
12 |
13 | /// Fetch the schema from uplink
14 | #[default]
15 | Uplink,
16 | }
17 |
```
--------------------------------------------------------------------------------
/.idea/runConfigurations/format___test___clippy.xml:
--------------------------------------------------------------------------------
```
1 | <component name="ProjectRunConfigurationManager">
2 | <configuration default="false" name="format - test - clippy" type="CompoundRunConfigurationType">
3 | <toRun name="Test apollo-mcp-server" type="CargoCommandRunConfiguration" />
4 | <toRun name="clippy" type="CargoCommandRunConfiguration" />
5 | <toRun name="format" type="CargoCommandRunConfiguration" />
6 | <method v="2" />
7 | </configuration>
8 | </component>
```
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "version": "2.0.0",
3 | "tasks": [{
4 | "label": "Generate Apollo Manifest",
5 | "command": "npx",
6 | "args": ["@apollo/generate-persisted-query-manifest","generate-persisted-query-manifest","--config","persisted_queries.config.json"],
7 | "type": "shell",
8 | "problemMatcher": [],
9 | "options": {
10 | "cwd": "${workspaceFolder}/graphql/TheSpaceDevs"
11 | }
12 | }]
13 | }
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/uplink/persisted_queries/event.rs:
--------------------------------------------------------------------------------
```rust
1 | use std::fmt::Debug;
2 | use std::fmt::Formatter;
3 |
4 | /// Persisted Query events
5 | pub enum Event {
6 | /// The persisted query manifest was updated
7 | UpdateManifest(Vec<(String, String)>),
8 | }
9 |
10 | impl Debug for Event {
11 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
12 | match self {
13 | Event::UpdateManifest(_) => {
14 | write!(f, "UpdateManifest(<redacted>)")
15 | }
16 | }
17 | }
18 | }
19 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/config_schema.rs:
--------------------------------------------------------------------------------
```rust
1 | //! Binary to output the JSON Schema for Apollo MCP Server configuration files
2 |
3 | // Most runtime code is unused by this binary
4 | #![allow(unused_imports, dead_code)]
5 |
6 | use anyhow::Context;
7 | use schemars::schema_for;
8 |
9 | mod runtime;
10 |
11 | fn main() -> anyhow::Result<()> {
12 | println!(
13 | "{}",
14 | serde_json::to_string_pretty(&schema_for!(runtime::Config))
15 | .with_context(|| "Failed to generate schema")?
16 | );
17 | Ok(())
18 | }
19 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/platform_api/operation_collections/error.rs:
--------------------------------------------------------------------------------
```rust
1 | use reqwest::header::{InvalidHeaderName, InvalidHeaderValue};
2 |
3 | #[derive(Debug, thiserror::Error)]
4 | pub enum CollectionError {
5 | #[error(transparent)]
6 | HeaderName(InvalidHeaderName),
7 |
8 | #[error(transparent)]
9 | HeaderValue(InvalidHeaderValue),
10 |
11 | #[error(transparent)]
12 | Request(reqwest::Error),
13 |
14 | #[error("Error in response: {0}")]
15 | Response(String),
16 |
17 | #[error("invalid variables: {0}")]
18 | InvalidVariables(String),
19 | }
20 |
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/config.yaml:
--------------------------------------------------------------------------------
```yaml
1 | endpoint: https://thespacedevs-production.up.railway.app/
2 | transport:
3 | type: streamable_http
4 | operations:
5 | source: local
6 | paths:
7 | - ./graphql/TheSpaceDevs/operations
8 | schema:
9 | source: local
10 | path: ./graphql/TheSpaceDevs/api.graphql
11 | overrides:
12 | mutation_mode: all
13 | introspection:
14 | execute:
15 | enabled: true
16 | introspect:
17 | enabled: true
18 | search:
19 | enabled: true
20 | validate:
21 | enabled: true
22 | cors:
23 | enabled: true
24 | allow_any_origin: true
25 |
```
--------------------------------------------------------------------------------
/xtask/src/lib.rs:
--------------------------------------------------------------------------------
```rust
1 | use std::convert::TryFrom;
2 | use std::env;
3 | use std::str;
4 |
5 | use camino::Utf8PathBuf;
6 | use once_cell::sync::Lazy;
7 |
8 | const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");
9 |
10 | pub static PKG_PROJECT_ROOT: Lazy<Utf8PathBuf> = Lazy::new(|| {
11 | let manifest_dir =
12 | Utf8PathBuf::try_from(MANIFEST_DIR).expect("could not get the root directory.");
13 | let root_dir = manifest_dir
14 | .ancestors()
15 | .nth(1)
16 | .expect("could not find project root");
17 |
18 | root_dir.to_path_buf()
19 | });
20 |
```
--------------------------------------------------------------------------------
/crates/apollo-schema-index/Cargo.toml:
--------------------------------------------------------------------------------
```toml
1 | [package]
2 | name = "apollo-schema-index"
3 | authors.workspace = true
4 | edition.workspace = true
5 | license-file.workspace = true
6 | repository.workspace = true
7 | rust-version.workspace = true
8 | version.workspace = true
9 |
10 | description = "GraphQL schema indexing"
11 |
12 | [dependencies]
13 | apollo-compiler.workspace = true
14 | enumset = "1.1.6"
15 | itertools = "0.14.0"
16 | tantivy = "0.24.2"
17 | thiserror.workspace = true
18 | tracing.workspace = true
19 |
20 | [dev-dependencies]
21 | insta.workspace = true
22 | rstest.workspace = true
23 |
24 | [lints]
25 | workspace = true
26 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/uplink/schema/schema_query.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query SupergraphSdlQuery(
2 | $apiKey: String!,
3 | $graphRef: String!,
4 | $ifAfterId: ID
5 | ) {
6 | routerConfig(
7 | ref: $graphRef,
8 | apiKey: $apiKey,
9 | ifAfterId: $ifAfterId
10 | ) {
11 | __typename
12 | ... on RouterConfigResult {
13 | id
14 | supergraphSDL
15 | minDelaySeconds
16 | }
17 | ... on Unchanged {
18 | id
19 | minDelaySeconds
20 | }
21 | ... on FetchError {
22 | code
23 | message
24 | }
25 | }
26 | }
27 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/uplink/persisted_queries/persisted_queries_manifest_query.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query PersistedQueriesManifestQuery(
2 | $apiKey: String!
3 | $graphRef: String!
4 | $ifAfterId: ID
5 | ) {
6 | persistedQueries(
7 | ref: $graphRef
8 | apiKey: $apiKey
9 | ifAfterId: $ifAfterId
10 | ) {
11 | __typename
12 | ... on PersistedQueriesResult {
13 | id
14 | minDelaySeconds
15 | chunks {
16 | id
17 | urls
18 | }
19 | }
20 | ... on Unchanged {
21 | id
22 | minDelaySeconds
23 | }
24 | ... on FetchError {
25 | code
26 | message
27 | }
28 | }
29 | }
```
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "rust-analyzer.check.command": "clippy",
3 | "[rust]": {
4 | "editor.defaultFormatter": "rust-lang.rust-analyzer",
5 | "editor.formatOnSave": true
6 | },
7 | "cSpell.words": [
8 | "apollographql",
9 | "clippy",
10 | "graphos",
11 | "insta",
12 | "peekable",
13 | "redactions",
14 | "reqwest",
15 | "rmcp",
16 | "rstest",
17 | "schemars",
18 | "serde",
19 | "splitn",
20 | "Streamable",
21 | "Subschema",
22 | "subschemas",
23 | "Supergraph",
24 | "thiserror",
25 | "webbrowser",
26 | "wiremock"
27 | ]
28 | }
29 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/lib.rs:
--------------------------------------------------------------------------------
```rust
1 | #![cfg_attr(coverage_nightly, feature(coverage_attribute))]
2 |
3 | pub mod auth;
4 | pub mod cors;
5 | pub mod custom_scalar_map;
6 | pub mod errors;
7 | pub mod event;
8 | mod explorer;
9 | mod graphql;
10 | pub mod headers;
11 | pub mod health;
12 | mod introspection;
13 | pub mod json_schema;
14 | pub(crate) mod meter;
15 | pub mod operations;
16 | pub mod sanitize;
17 | pub(crate) mod schema_tree_shake;
18 | pub mod server;
19 | pub mod telemetry_attributes;
20 |
21 | /// These values are generated at build time by build.rs using telemetry.toml as input.
22 | pub mod generated {
23 | pub mod telemetry {
24 | include!(concat!(env!("OUT_DIR"), "/telemetry_attributes.rs"));
25 | }
26 | }
27 |
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/.vscode/tasks.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Generate Apollo Manifest",
6 | "command": "npx",
7 | "args": [
8 | "@apollo/generate-persisted-query-manifest",
9 | "generate-persisted-query-manifest",
10 | "--config",
11 | "persisted_queries.config.json"
12 | ],
13 | "type": "shell",
14 | "problemMatcher": []
15 | },
16 | {
17 | "label": "Run rover dev",
18 | "command": "rover",
19 | "args": [
20 | "dev",
21 | "--supergraph-config",
22 | "supergraph.yaml",
23 | "--mcp"
24 | ],
25 | "type": "shell",
26 | "problemMatcher": []
27 | }
28 | ]
29 | }
30 |
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/local-operations/operations/ExploreCelestialBodies.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query ExploreCelestialBodies($search: String, $limit: Int = 10, $offset: Int = 0) {
2 | celestialBodies(search: $search, limit: $limit, offset: $offset) {
3 | pageInfo {
4 | count
5 | next
6 | previous
7 | }
8 | results {
9 | id
10 | name
11 |
12 | # Physical characteristics
13 | diameter # in kilometers
14 | mass # in kilograms
15 | gravity # in m/s²
16 | lengthOfDay
17 | atmosphere
18 |
19 | # Classification
20 | type {
21 | id
22 | name
23 | }
24 |
25 | # Visual and descriptive content
26 | image {
27 | url
28 | thumbnail
29 | credit
30 | }
31 | description
32 | wikiUrl
33 | }
34 | }
35 | }
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/operations/ExploreCelestialBodies.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query ExploreCelestialBodies($search: String, $limit: Int = 10, $offset: Int = 0) {
2 | celestialBodies(search: $search, limit: $limit, offset: $offset) {
3 | pageInfo {
4 | count
5 | next
6 | previous
7 | }
8 | results {
9 | id
10 | name
11 |
12 | # Physical characteristics
13 | diameter # in kilometers
14 | mass # in kilograms
15 | gravity # in m/s²
16 | lengthOfDay
17 | atmosphere
18 |
19 | # Classification
20 | type {
21 | id
22 | name
23 | }
24 |
25 | # Visual and descriptive content
26 | image {
27 | url
28 | thumbnail
29 | credit
30 | }
31 | description
32 | wikiUrl
33 | }
34 | }
35 | }
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/overrides.rs:
--------------------------------------------------------------------------------
```rust
1 | use apollo_mcp_server::operations::MutationMode;
2 | use schemars::JsonSchema;
3 | use serde::Deserialize;
4 |
5 | /// Overridable flags
6 | #[derive(Debug, Deserialize, Default, JsonSchema)]
7 | #[serde(default)]
8 | pub struct Overrides {
9 | /// Disable type descriptions to save on context-window space
10 | pub disable_type_description: bool,
11 |
12 | /// Disable schema descriptions to save on context-window space
13 | pub disable_schema_description: bool,
14 |
15 | /// Expose a tool that returns the URL to open a GraphQL operation in Apollo Explorer (requires APOLLO_GRAPH_REF)
16 | pub enable_explorer: bool,
17 |
18 | /// Set the mutation mode access level for the MCP server
19 | pub mutation_mode: MutationMode,
20 | }
21 |
```
--------------------------------------------------------------------------------
/xtask/src/commands/changeset/matching_pull_request.graphql:
--------------------------------------------------------------------------------
```graphql
1 | # This operation is used to generate Rust code which lives in a file directly
2 | # next to this with the same name but a `.rs` extension. For instructions on
3 | # how to generate the code, see the top of `./mod.rs`.
4 | fragment PrInfo on PullRequest {
5 | url
6 | number
7 | author {
8 | __typename
9 | login
10 | }
11 | title
12 | closingIssuesReferences(last: 4) {
13 | nodes {
14 | url
15 | number
16 | repository {
17 | nameWithOwner
18 | }
19 | }
20 | }
21 | body
22 | }
23 | fragment PrSearchResult on SearchResultItemConnection {
24 | issueCount
25 | nodes {
26 | __typename
27 | ...PrInfo
28 | }
29 | }
30 |
31 | query MatchingPullRequest($search: String!) {
32 | search(
33 | type: ISSUE
34 | query: $search
35 | first: 1
36 | ) {
37 | ...PrSearchResult
38 | }
39 | }
40 |
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/local-operations/operations/GetAstronautDetails.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query GetAstronautDetails($astronautId: ID!) {
2 | astronaut(id: $astronautId) {
3 | id
4 | name
5 | status
6 | inSpace
7 | age
8 |
9 | # Birth and career dates
10 | dateOfBirth
11 | dateOfDeath
12 | firstFlight
13 | lastFlight
14 |
15 | # Space experience metrics
16 | timeInSpace
17 | evaTime # Extravehicular Activity time
18 |
19 | # Agency information
20 | agency {
21 | id
22 | name
23 | abbrev
24 | country {
25 | name
26 | nationalityName
27 | }
28 | }
29 |
30 | # Nationality
31 | nationality {
32 | name
33 | nationalityName
34 | alpha2Code
35 | }
36 |
37 | # Media
38 | image {
39 | url
40 | thumbnail
41 | credit
42 | }
43 |
44 | # Bio and links
45 | bio
46 | wiki
47 |
48 | # Social media
49 | socialMediaLinks {
50 | url
51 | socialMedia {
52 | name
53 | url
54 | }
55 | }
56 | }
57 | }
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/operations/GetAstronautDetails.graphql:
--------------------------------------------------------------------------------
```graphql
1 | query GetAstronautDetails($astronautId: ID!) {
2 | astronaut(id: $astronautId) {
3 | id
4 | name
5 | status
6 | inSpace
7 | age
8 |
9 | # Birth and career dates
10 | dateOfBirth
11 | dateOfDeath
12 | firstFlight
13 | lastFlight
14 |
15 | # Space experience metrics
16 | timeInSpace
17 | evaTime # Extravehicular Activity time
18 |
19 | # Agency information
20 | agency {
21 | id
22 | name
23 | abbrev
24 | country {
25 | name
26 | nationalityName
27 | }
28 | }
29 |
30 | # Nationality
31 | nationality {
32 | name
33 | nationalityName
34 | alpha2Code
35 | }
36 |
37 | # Media
38 | image {
39 | url
40 | thumbnail
41 | credit
42 | }
43 |
44 | # Bio and links
45 | bio
46 | wiki
47 |
48 | # Social media
49 | socialMediaLinks {
50 | url
51 | socialMedia {
52 | name
53 | url
54 | }
55 | }
56 | }
57 | }
```
--------------------------------------------------------------------------------
/xtask/src/main.rs:
--------------------------------------------------------------------------------
```rust
1 | mod commands;
2 |
3 | use anyhow::Result;
4 | use clap::Parser;
5 | use nu_ansi_term::Color::Green;
6 |
7 | fn main() -> Result<()> {
8 | let app = Xtask::parse();
9 | app.run()
10 | }
11 |
12 | #[derive(Debug, clap::Parser)]
13 | #[structopt(
14 | name = "xtask",
15 | about = "Workflows used locally and in CI for developing the Apollo MCP Server"
16 | )]
17 | struct Xtask {
18 | #[command(subcommand)]
19 | pub command: Command,
20 | }
21 |
22 | #[derive(Debug, clap::Subcommand)]
23 | pub enum Command {
24 | /// Produce or consume changesets
25 | #[command(subcommand)]
26 | Changeset(commands::changeset::Command),
27 | }
28 |
29 | impl Xtask {
30 | pub fn run(&self) -> Result<()> {
31 | match &self.command {
32 | Command::Changeset(command) => command.run(),
33 | }?;
34 | eprintln!("{}", Green.bold().paint("Success!"));
35 | Ok(())
36 | }
37 | }
38 |
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/local-operations/operations/SearchUpcomingLaunches.graphql:
--------------------------------------------------------------------------------
```graphql
1 | # Fields searched - launch_designator, launch_service_provider__name, mission__name, name, pad__location__name, pad__name, rocket__configuration__manufacturer__abbrev, rocket__configuration__manufacturer__name, rocket__configuration__name, rocket__spacecraftflight__spacecraft__name. Codes are the best search terms to use. Single words are the next best alternative when you cannot use a code to search
2 | query SearchUpcomingLaunches($query: String!) {
3 | upcomingLaunches(limit: 20, search: $query){
4 | pageInfo {
5 | count
6 | }
7 | results {
8 | id
9 | name
10 | weatherConcerns
11 | rocket {
12 | id
13 | configuration {
14 | fullName
15 | }
16 | }
17 | mission {
18 | name
19 | description
20 | }
21 | webcastLive
22 | provider {
23 | name
24 | }
25 | }
26 | }
27 | }
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/operations/SearchUpcomingLaunches.graphql:
--------------------------------------------------------------------------------
```graphql
1 | # Fields searched - launch_designator, launch_service_provider__name, mission__name, name, pad__location__name, pad__name, rocket__configuration__manufacturer__abbrev, rocket__configuration__manufacturer__name, rocket__configuration__name, rocket__spacecraftflight__spacecraft__name. Codes are the best search terms to use. Single words are the next best alternative when you cannot use a code to search
2 | query SearchUpcomingLaunches($query: String!) {
3 | upcomingLaunches(limit: 20, search: $query){
4 | pageInfo {
5 | count
6 | }
7 | results {
8 | id
9 | name
10 | weatherConcerns
11 | rocket {
12 | id
13 | configuration {
14 | fullName
15 | }
16 | }
17 | mission {
18 | name
19 | description
20 | }
21 | webcastLive
22 | provider {
23 | name
24 | }
25 | }
26 | }
27 | }
```
--------------------------------------------------------------------------------
/docs/source/custom-scalars.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Custom Scalars Configuration
3 | ---
4 |
5 | ## Custom scalars configuration
6 |
7 | You can specify a custom scalars configuration JSON file to map a custom scalar to a [JSON schema type](https://json-schema.org/understanding-json-schema/reference/type). The JSON file is an object with custom scalar names as keys and JSON schema types as values:
8 |
9 | ```json
10 | {
11 | "MyCustomScalar": { "type": "string" }
12 | }
13 | ```
14 |
15 | Other than JSON schema type, an overriding description can also be provided. In the following example the description provided in the schema, `scalar description`, would get overridden by the description found in the custom scalar configuration file, `override description`:
16 |
17 | ```graphql
18 | """
19 | scalar description
20 | """
21 | scalar MyCustomScalar
22 | ```
23 |
24 | ```json
25 | {
26 | "MyCustomScalar": { "type": "string", "description": "override description" }
27 | }
28 | ```
29 |
```
--------------------------------------------------------------------------------
/.idea/runConfigurations/clippy.xml:
--------------------------------------------------------------------------------
```
1 | <component name="ProjectRunConfigurationManager">
2 | <configuration default="false" name="clippy" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
3 | <option name="command" value="clippy" />
4 | <option name="workingDirectory" value="file://$PROJECT_DIR$" />
5 | <envs />
6 | <option name="emulateTerminal" value="true" />
7 | <option name="channel" value="DEFAULT" />
8 | <option name="requiredFeatures" value="true" />
9 | <option name="allFeatures" value="false" />
10 | <option name="withSudo" value="false" />
11 | <option name="buildTarget" value="REMOTE" />
12 | <option name="backtrace" value="SHORT" />
13 | <option name="isRedirectInput" value="false" />
14 | <option name="redirectInputPath" value="" />
15 | <method v="2">
16 | <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
17 | </method>
18 | </configuration>
19 | </component>
```
--------------------------------------------------------------------------------
/.idea/runConfigurations/format.xml:
--------------------------------------------------------------------------------
```
1 | <component name="ProjectRunConfigurationManager">
2 | <configuration default="false" name="format" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
3 | <option name="command" value="fmt --all" />
4 | <option name="workingDirectory" value="file://$PROJECT_DIR$" />
5 | <envs />
6 | <option name="emulateTerminal" value="true" />
7 | <option name="channel" value="DEFAULT" />
8 | <option name="requiredFeatures" value="true" />
9 | <option name="allFeatures" value="false" />
10 | <option name="withSudo" value="false" />
11 | <option name="buildTarget" value="REMOTE" />
12 | <option name="backtrace" value="SHORT" />
13 | <option name="isRedirectInput" value="false" />
14 | <option name="redirectInputPath" value="" />
15 | <method v="2">
16 | <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
17 | </method>
18 | </configuration>
19 | </component>
```
--------------------------------------------------------------------------------
/.idea/runConfigurations/Test_apollo_mcp_server.xml:
--------------------------------------------------------------------------------
```
1 | <component name="ProjectRunConfigurationManager">
2 | <configuration default="false" name="Test apollo-mcp-server" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
3 | <option name="buildProfileId" value="test" />
4 | <option name="command" value="test --workspace" />
5 | <option name="workingDirectory" value="file://$PROJECT_DIR$" />
6 | <envs />
7 | <option name="emulateTerminal" value="true" />
8 | <option name="channel" value="DEFAULT" />
9 | <option name="requiredFeatures" value="true" />
10 | <option name="allFeatures" value="false" />
11 | <option name="withSudo" value="false" />
12 | <option name="buildTarget" value="REMOTE" />
13 | <option name="backtrace" value="SHORT" />
14 | <option name="isRedirectInput" value="false" />
15 | <option name="redirectInputPath" value="" />
16 | <method v="2">
17 | <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
18 | </method>
19 | </configuration>
20 | </component>
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/sanitize.rs:
--------------------------------------------------------------------------------
```rust
1 | //! Provide sanitized type definitions suitable for an AI model.
2 | //! For example, remove directives from GraphQL schema types.
3 | use apollo_compiler::schema::{EnumType, FieldDefinition, ObjectType, ScalarType, UnionType};
4 | use apollo_compiler::{ast, schema};
5 |
6 | pub trait Sanitize<T> {
7 | fn sanitize(self) -> T;
8 | }
9 |
10 | // Implementation for all schema directive types
11 | macro_rules! impl_sanitize {
12 | ($type:ty, $directive_list_type:path) => {
13 | impl Sanitize<$type> for $type {
14 | fn sanitize(self) -> Self {
15 | Self {
16 | directives: $directive_list_type(vec![]),
17 | ..self
18 | }
19 | }
20 | }
21 | };
22 | }
23 |
24 | impl_sanitize!(EnumType, schema::DirectiveList);
25 | impl_sanitize!(FieldDefinition, ast::DirectiveList);
26 | impl_sanitize!(ObjectType, schema::DirectiveList);
27 | impl_sanitize!(UnionType, schema::DirectiveList);
28 | impl_sanitize!(ScalarType, schema::DirectiveList);
29 |
```
--------------------------------------------------------------------------------
/.idea/runConfigurations/Run_spacedevs.xml:
--------------------------------------------------------------------------------
```
1 | <component name="ProjectRunConfigurationManager">
2 | <configuration default="false" name="Run spacedevs" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
3 | <option name="command" value="run --package apollo-mcp-server --bin apollo-mcp-server -- ./graphql/TheSpaceDevs/config.yaml" />
4 | <option name="workingDirectory" value="file://$PROJECT_DIR$" />
5 | <envs />
6 | <option name="emulateTerminal" value="true" />
7 | <option name="channel" value="DEFAULT" />
8 | <option name="requiredFeatures" value="true" />
9 | <option name="allFeatures" value="false" />
10 | <option name="withSudo" value="false" />
11 | <option name="buildTarget" value="REMOTE" />
12 | <option name="backtrace" value="SHORT" />
13 | <option name="isRedirectInput" value="false" />
14 | <option name="redirectInputPath" value="" />
15 | <method v="2">
16 | <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
17 | </method>
18 | </configuration>
19 | </component>
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/server/states/schema_configured.rs:
--------------------------------------------------------------------------------
```rust
1 | use apollo_compiler::{Schema, validation::Valid};
2 | use tracing::debug;
3 |
4 | use crate::{errors::ServerError, operations::RawOperation};
5 |
6 | use super::{Config, Starting};
7 |
8 | pub(super) struct SchemaConfigured {
9 | pub(super) config: Config,
10 | pub(super) schema: Valid<Schema>,
11 | }
12 |
13 | impl SchemaConfigured {
14 | pub(super) async fn set_schema(
15 | self,
16 | schema: Valid<Schema>,
17 | ) -> Result<SchemaConfigured, ServerError> {
18 | debug!("Received schema:\n{}", schema);
19 | Ok(SchemaConfigured { schema, ..self })
20 | }
21 |
22 | pub(super) async fn set_operations(
23 | self,
24 | operations: Vec<RawOperation>,
25 | ) -> Result<Starting, ServerError> {
26 | debug!(
27 | "Received {} operations:\n{}",
28 | operations.len(),
29 | serde_json::to_string_pretty(&operations)?
30 | );
31 | Ok(Starting {
32 | config: self.config,
33 | schema: self.schema,
34 | operations,
35 | })
36 | }
37 | }
38 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/logging/parsers.rs:
--------------------------------------------------------------------------------
```rust
1 | use std::{fmt::Display, marker::PhantomData, str::FromStr};
2 |
3 | use serde::Deserializer;
4 |
5 | pub(crate) fn from_str<'de, D, T>(deserializer: D) -> Result<T, D::Error>
6 | where
7 | D: Deserializer<'de>,
8 | T: FromStr,
9 | <T as FromStr>::Err: Display,
10 | {
11 | struct FromStrVisitor<Inner> {
12 | _phantom: PhantomData<Inner>,
13 | }
14 | impl<Inner> serde::de::Visitor<'_> for FromStrVisitor<Inner>
15 | where
16 | Inner: FromStr,
17 | <Inner as FromStr>::Err: Display,
18 | {
19 | type Value = Inner;
20 |
21 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
22 | formatter.write_str("a string")
23 | }
24 |
25 | fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
26 | where
27 | E: serde::de::Error,
28 | {
29 | Inner::from_str(v).map_err(|e| serde::de::Error::custom(e.to_string()))
30 | }
31 | }
32 |
33 | deserializer.deserialize_str(FromStrVisitor {
34 | _phantom: PhantomData,
35 | })
36 | }
37 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/server/states/configuring.rs:
--------------------------------------------------------------------------------
```rust
1 | use apollo_compiler::{Schema, validation::Valid};
2 | use tracing::debug;
3 |
4 | use crate::{errors::ServerError, operations::RawOperation};
5 |
6 | use super::{Config, OperationsConfigured, SchemaConfigured};
7 |
8 | pub(super) struct Configuring {
9 | pub(super) config: Config,
10 | }
11 |
12 | impl Configuring {
13 | pub(super) async fn set_schema(
14 | self,
15 | schema: Valid<Schema>,
16 | ) -> Result<SchemaConfigured, ServerError> {
17 | debug!("Received schema:\n{}", schema);
18 | Ok(SchemaConfigured {
19 | config: self.config,
20 | schema,
21 | })
22 | }
23 |
24 | pub(super) async fn set_operations(
25 | self,
26 | operations: Vec<RawOperation>,
27 | ) -> Result<OperationsConfigured, ServerError> {
28 | debug!(
29 | "Received {} operations:\n{}",
30 | operations.len(),
31 | serde_json::to_string_pretty(&operations)?
32 | );
33 | Ok(OperationsConfigured {
34 | config: self.config,
35 | operations,
36 | })
37 | }
38 | }
39 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/server/states/operations_configured.rs:
--------------------------------------------------------------------------------
```rust
1 | use apollo_compiler::{Schema, validation::Valid};
2 | use tracing::debug;
3 |
4 | use crate::{errors::ServerError, operations::RawOperation, server::states::Starting};
5 |
6 | use super::Config;
7 |
8 | pub(super) struct OperationsConfigured {
9 | pub(super) config: Config,
10 | pub(super) operations: Vec<RawOperation>,
11 | }
12 |
13 | impl OperationsConfigured {
14 | pub(super) async fn set_schema(self, schema: Valid<Schema>) -> Result<Starting, ServerError> {
15 | debug!("Received schema:\n{}", schema);
16 | Ok(Starting {
17 | config: self.config,
18 | operations: self.operations,
19 | schema,
20 | })
21 | }
22 |
23 | pub(super) async fn set_operations(
24 | self,
25 | operations: Vec<RawOperation>,
26 | ) -> Result<OperationsConfigured, ServerError> {
27 | debug!(
28 | "Received {} operations:\n{}",
29 | operations.len(),
30 | serde_json::to_string_pretty(&operations)?
31 | );
32 | Ok(OperationsConfigured { operations, ..self })
33 | }
34 | }
35 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/telemetry.toml:
--------------------------------------------------------------------------------
```toml
1 | [attributes.apollo.mcp]
2 | tool_name = "The tool name"
3 | operation_id = "The operation id - either persisted query id, operation name, or unknown"
4 | operation_source = "The operation source - either operation (local file/op collection), persisted query, or LLM generated"
5 | request_id = "The request id"
6 | success = "Sucess flag indicator"
7 | raw_operation = "Graphql operation text and metadata used for Tool generation"
8 | client_name = "The client name that initializes with the MCP Server"
9 | client_version = "The client version that initializes with the MCP Server"
10 |
11 | [metrics.apollo.mcp]
12 | "initialize.count" = "Number of times initialize has been called"
13 | "tool.count" = "Number of times call_tool has been called"
14 | "tool.duration" = "Duration of call_tool"
15 | "list_tools.count" = "Number of times list_tools has been called"
16 | "get_info.count" = "Number of times get_info has been called"
17 | "operation.duration" = "Duration of graphql execute"
18 | "operation.count" = "Number of times graphql execute has been called"
19 |
```
--------------------------------------------------------------------------------
/graphql/weather/persisted_queries/apollo.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "format": "apollo-persisted-query-manifest",
3 | "version": 1,
4 | "operations": [
5 | {
6 | "id": "f4d7c9e3dca95d72be8b2ae5df7db1a92a29d8c2f43c1d3e04e30e7eb0fb23d",
7 | "clientName": "my-web-app",
8 | "body": "query GetAlerts($state: String!) { alerts(state: $state) { severity description instruction } }",
9 | "name": "GetAlerts",
10 | "type": "query"
11 | },
12 | {
13 | "id": "e2c1b89a5e4d95f6b8f7dfed7d9db192ea39d0cb34b3d4cd1bd7e0fbec23efb",
14 | "clientName": "my-web-app",
15 | "body": "query GetAllWeatherData($coordinate: InputCoordinate!, $state: String!) { forecast(coordinate: $coordinate) { detailed } alerts(state: $state) { severity description instruction } }",
16 | "name": "GetAllWeatherData",
17 | "type": "query"
18 | },
19 | {
20 | "id": "7f4c9e3dca95d72be8b2ae5df7db1a92a29d8c2f43c1d3e04e30e7eb0fb23d",
21 | "clientName": "my-web-app",
22 | "body": "query GetForecast($coordinate: InputCoordinate!) { forecast(coordinate: $coordinate) { detailed } }",
23 | "name": "GetForecast",
24 | "type": "query"
25 | }
26 | ]
27 | }
28 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/Cargo.toml:
--------------------------------------------------------------------------------
```toml
1 | [package]
2 | name = "apollo-mcp-registry"
3 | authors.workspace = true
4 | edition.workspace = true
5 | license-file.workspace = true
6 | repository.workspace = true
7 | rust-version.workspace = true
8 | version.workspace = true
9 |
10 | description = "Registry providing schema and operations to the MCP Server"
11 |
12 | [dependencies]
13 | derive_more = { version = "2.0.1", default-features = false, features = [
14 | "from",
15 | "display",
16 | ] }
17 | educe = "0.6.0"
18 | futures.workspace = true
19 | graphql_client = "0.14.0"
20 | insta.workspace = true
21 | notify = "8.0.0"
22 | reqwest.workspace = true
23 | secrecy.workspace = true
24 | serde.workspace = true
25 | serde_json.workspace = true
26 | thiserror.workspace = true
27 | tokio.workspace = true
28 | tokio-stream.workspace = true
29 | tower = "0.5.2"
30 | tracing.workspace = true
31 | url.workspace = true
32 | uuid = { version = "1.16.0", features = ["serde", "v4"] }
33 | tracing-core.workspace = true
34 | tracing-subscriber.workspace = true
35 |
36 | [dev-dependencies]
37 | test-log = { version = "0.2.16", default-features = false, features = [
38 | "trace",
39 | ] }
40 | tracing-futures = { version = "0.2.5", features = ["futures-03"] }
41 | wiremock = "0.6.3"
42 |
43 | [lints]
44 | workspace = true
45 |
```
--------------------------------------------------------------------------------
/docs/source/_sidebar.yaml:
--------------------------------------------------------------------------------
```yaml
1 | switcher:
2 | heading: "Apollo MCP Server"
3 | versions:
4 | - label: v1
5 | latest: true
6 | href: ./
7 | defaultOpenDepth: 2
8 | items:
9 | - label: "Overview"
10 | href: "."
11 | - label: "Quickstart"
12 | href: "./quickstart"
13 | - label: "Define tools"
14 | href: "./define-tools"
15 | - label: "Configuration"
16 | children:
17 | - label: "YAML Config Reference"
18 | href: "./config-file"
19 | - label: "Custom Scalars"
20 | href: "./custom-scalars"
21 | - label: "Run the MCP Server"
22 | href: "./run"
23 | - label: "Debugging"
24 | href: "./debugging"
25 | - label: "Deployment"
26 | children:
27 | - label: "Overview"
28 | href: "./deploy"
29 | - label: "Health Checks"
30 | href: "./health-checks"
31 | - label: "CORS"
32 | href: "./cors"
33 | - label: "Authorization"
34 | href: "./auth"
35 | - label: "Telemetry"
36 | href: "./telemetry"
37 | - label: "Best Practices"
38 | href: "./best-practices"
39 | - label: "Licensing"
40 | href: "./licensing"
41 | - label: "Limitations"
42 | href: "./limitations"
43 | - label: "Guides"
44 | children:
45 | - label: "Authorization with Auth0"
46 | href: "./guides/auth-auth0"
47 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/auth/protected_resource.rs:
--------------------------------------------------------------------------------
```rust
1 | use serde::Serialize;
2 | use url::Url;
3 |
4 | use super::Config;
5 |
6 | /// OAuth 2.1 Protected Resource Response
7 | // TODO: This might be better found in an existing rust crate (or contributed upstream to one)
8 | #[derive(Serialize)]
9 | pub(super) struct ProtectedResource {
10 | /// The URL of the resource
11 | resource: Url,
12 |
13 | /// List of authorization servers protecting this resource
14 | authorization_servers: Vec<Url>,
15 |
16 | /// List of authentication methods allowed
17 | bearer_methods_supported: Vec<String>,
18 |
19 | /// Scopes allowed to request from the authorization servers
20 | scopes_supported: Vec<String>,
21 |
22 | /// Link to documentation about this resource
23 | #[serde(skip_serializing_if = "Option::is_none")]
24 | resource_documentation: Option<Url>,
25 | }
26 |
27 | impl From<Config> for ProtectedResource {
28 | fn from(value: Config) -> Self {
29 | Self {
30 | resource: value.resource,
31 | authorization_servers: value.servers,
32 | bearer_methods_supported: vec!["header".to_string()], // The spec only supports header auth
33 | scopes_supported: value.scopes,
34 | resource_documentation: value.resource_documentation,
35 | }
36 | }
37 | }
38 |
```
--------------------------------------------------------------------------------
/xtask/Cargo.toml:
--------------------------------------------------------------------------------
```toml
1 | [workspace]
2 |
3 | [package]
4 | name = "xtask"
5 | version = "1.5.0"
6 | authors = ["Apollo Graph, Inc. <[email protected]>"]
7 | edition = "2021"
8 | publish = false
9 |
10 | [dependencies]
11 | anyhow = "1"
12 | camino = "1"
13 | clap = { version = "4.5.1", features = ["derive"] }
14 | cargo_metadata = "0.19.0"
15 | # Only use the `clock` features of `chrono` to avoid the `time` dependency
16 | # impacted by CVE-2020-26235. https://github.com/chronotope/chrono/issues/602
17 | # and https://github.com/chronotope/chrono/issues/1073 will explain more.
18 | chrono = { version = "0.4.34", default-features = false, features = ["clock"] }
19 | console = "0.15.8"
20 | dialoguer = "0.11.0"
21 | graphql_client = "0.14.0"
22 | itertools = "0.14.0"
23 | libc = "0.2"
24 | memorable-wordlist = "0.1.7"
25 | nu-ansi-term = "0.50"
26 | once_cell = "1"
27 | regex = "1.10.3"
28 | reqwest = { version = "0.11", default-features = false, features = [
29 | "blocking",
30 | "json",
31 | "rustls-tls",
32 | "rustls-tls-native-roots",
33 | ] }
34 | serde = { version = "1.0.197", features = ["derive"] }
35 | serde_json = "1"
36 | tempfile = "3"
37 | tinytemplate = "1.2.1"
38 | tokio = { version = "1.36.0", features = ["full"] }
39 | which = "7.0.0"
40 |
41 | [dev-dependencies]
42 | insta = { version = "1.43.1", features = ["json", "redactions", "yaml"] }
43 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/operations/schema_walker.rs:
--------------------------------------------------------------------------------
```rust
1 | //! JSON Schema generation utilities
2 | //!
3 | //! The types in this module generate JSON schemas for GraphQL types by walking
4 | //! the types recursively.
5 |
6 | use apollo_compiler::{Schema as GraphQLSchema, ast::Type};
7 | use schemars::Schema;
8 | use serde_json::{Map, Value};
9 |
10 | use crate::custom_scalar_map::CustomScalarMap;
11 |
12 | mod name;
13 | mod r#type;
14 |
15 | /// Convert a GraphQL type into a JSON Schema.
16 | ///
17 | /// Note: This is recursive, which might cause a stack overflow if the type is
18 | /// sufficiently nested / complex.
19 | pub fn type_to_schema(
20 | r#type: &Type,
21 | schema: &GraphQLSchema,
22 | definitions: &mut Map<String, Value>,
23 | custom_scalar_map: Option<&CustomScalarMap>,
24 | description: Option<String>,
25 | ) -> Schema {
26 | r#type::Type {
27 | cache: definitions,
28 | custom_scalar_map,
29 | description: &description,
30 | schema,
31 | r#type,
32 | }
33 | .into()
34 | }
35 |
36 | /// Modifies a schema to include an optional description
37 | fn with_desc(mut schema: Schema, description: &Option<String>) -> Schema {
38 | if let Some(desc) = description {
39 | schema
40 | .ensure_object()
41 | .entry("description")
42 | .or_insert(desc.clone().into());
43 | }
44 |
45 | schema
46 | }
47 |
```
--------------------------------------------------------------------------------
/.github/workflows/canary-release.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Canary Release
2 | on:
3 | push:
4 | # don't run on tags, run on commits
5 | # https://github.com/orgs/community/discussions/25615
6 | tags-ignore:
7 | - "**"
8 | paths-ignore:
9 | - '.github/**'
10 | - '.cargo/**'
11 | - '.direnv/**'
12 | - '.vscode/**'
13 | - 'docs/**'
14 | - 'Cargo.*'
15 | - 'crates/**/Cargo.*'
16 | - '*.md'
17 | branches:
18 | - develop
19 | workflow_dispatch:
20 |
21 | permissions:
22 | contents: read
23 | packages: write
24 |
25 | concurrency:
26 | group: canary-${{ github.ref }}
27 | cancel-in-progress: true
28 |
29 | jobs:
30 | compute_canary_version:
31 | runs-on: ubuntu-24.04
32 | outputs:
33 | version: ${{ steps.canary_version.outputs.version }}
34 | steps:
35 | - name: Compute canary version
36 | id: canary_version
37 | run: |
38 | SHORT_SHA=${GITHUB_SHA::7}
39 | DATE=$(date -u +%Y%m%dT%H%M%SZ)
40 | echo "version=canary-${DATE}-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
41 |
42 | release_canary_container:
43 | needs: compute_canary_version
44 | permissions:
45 | contents: read
46 | packages: write
47 | attestations: write
48 | id-token: write
49 | uses: ./.github/workflows/release-container.yml
50 | with:
51 | version: ${{ needs.compute_canary_version.outputs.version }}
52 | secrets: inherit
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/auth/www_authenticate.rs:
--------------------------------------------------------------------------------
```rust
1 | //! WWW Authenticate header definition.
2 | //!
3 | //! TODO: This might be nice to upstream to hyper.
4 |
5 | use headers::{Header, HeaderValue};
6 | use http::header::WWW_AUTHENTICATE;
7 | use tracing::warn;
8 | use url::Url;
9 |
10 | pub(super) enum WwwAuthenticate {
11 | Bearer { resource_metadata: Url },
12 | }
13 |
14 | impl Header for WwwAuthenticate {
15 | fn name() -> &'static http::HeaderName {
16 | &WWW_AUTHENTICATE
17 | }
18 |
19 | fn decode<'i, I>(_values: &mut I) -> Result<Self, headers::Error>
20 | where
21 | Self: Sized,
22 | I: Iterator<Item = &'i http::HeaderValue>,
23 | {
24 | // We don't care about decoding, so we do nothing here.
25 | Err(headers::Error::invalid())
26 | }
27 |
28 | fn encode<E: Extend<http::HeaderValue>>(&self, values: &mut E) {
29 | let encoded = match &self {
30 | WwwAuthenticate::Bearer { resource_metadata } => format!(
31 | r#"Bearer resource_metadata="{}""#,
32 | resource_metadata.as_str()
33 | ),
34 | };
35 |
36 | // TODO: This shouldn't error, but it can so we might need to do something else here
37 | match HeaderValue::from_str(&encoded) {
38 | Ok(value) => values.extend(std::iter::once(value)),
39 | Err(e) => warn!("could not construct WWW-AUTHENTICATE header: {e}"),
40 | }
41 | }
42 | }
43 |
```
--------------------------------------------------------------------------------
/graphql/weather/api.graphql:
--------------------------------------------------------------------------------
```graphql
1 | """A weather alert"""
2 | type Alert {
3 | """The severity of this alert"""
4 | severity: String
5 |
6 | """A description of the alert"""
7 | description: String
8 |
9 | """Information about how people should respond to the alert"""
10 | instruction: String
11 | }
12 |
13 | """A coordinate, consisting of a latitude and longitude"""
14 | type Coordinate {
15 | """The latitude of this coordinate"""
16 | latitude: String!
17 |
18 | """The longitude of this coordinate"""
19 | longitude: String!
20 | }
21 |
22 | """A weather forecast"""
23 | type Forecast {
24 | """The coordinate associated with this forecast"""
25 | coordinate: Coordinate!
26 |
27 | """
28 | The National Weather Service (NWS) URL where the forecast data can be read
29 | """
30 | forecastURL: String!
31 |
32 | """A detailed weather forecast from the National Weather Service (NWS)"""
33 | detailed: String!
34 | }
35 |
36 | """A coordinate, consisting of a latitude and longitude"""
37 | input InputCoordinate {
38 | """The latitude of this coordinate"""
39 | latitude: String!
40 |
41 | """The longitude of this coordinate"""
42 | longitude: String!
43 | }
44 |
45 | type Query {
46 | """Get the weather forecast for a coordinate"""
47 | forecast(coordinate: InputCoordinate!): Forecast
48 |
49 | """
50 | Get the weather alerts for a state
51 | """
52 | alerts(
53 | """The two-letter state abbreviation (e.g., 'CO' for Colorado)"""
54 | state: String!
55 | ): [Alert]
56 | }
57 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/uplink/schema/event.rs:
--------------------------------------------------------------------------------
```rust
1 | use super::SchemaState;
2 | use std::fmt::Debug;
3 | use std::fmt::Formatter;
4 | use std::fmt::Result;
5 |
6 | /// Schema events
7 | pub enum Event {
8 | /// The schema was updated.
9 | UpdateSchema(SchemaState),
10 |
11 | /// There are no more updates to the schema
12 | NoMoreSchema,
13 | }
14 |
15 | impl Debug for Event {
16 | fn fmt(&self, f: &mut Formatter) -> Result {
17 | match self {
18 | Event::UpdateSchema(_) => {
19 | write!(f, "UpdateSchema(<redacted>)")
20 | }
21 | Event::NoMoreSchema => {
22 | write!(f, "NoMoreSchema")
23 | }
24 | }
25 | }
26 | }
27 |
28 | #[cfg(test)]
29 | mod tests {
30 | use super::*;
31 |
32 | #[test]
33 | fn test_debug_event_no_more_schema() {
34 | let event = Event::NoMoreSchema;
35 | let output = format!("{:?}", event);
36 | assert_eq!(output, "NoMoreSchema");
37 | }
38 |
39 | #[test]
40 | fn test_debug_redacts_update_schema() {
41 | let event = Event::UpdateSchema(SchemaState {
42 | sdl: "type Query { hello: String }".to_string(),
43 | launch_id: Some("test-launch-123".to_string()),
44 | });
45 |
46 | let output = format!("{:?}", event);
47 | assert_eq!(output, "UpdateSchema(<redacted>)");
48 | assert!(!output.contains("type Query"));
49 | assert!(!output.contains("test-launch-123"));
50 | }
51 | }
52 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/logging/log_rotation_kind.rs:
--------------------------------------------------------------------------------
```rust
1 | use schemars::JsonSchema;
2 | use serde::Deserialize;
3 | use tracing_appender::rolling::Rotation;
4 |
5 | #[derive(Debug, Deserialize, JsonSchema, Clone)]
6 | pub enum LogRotationKind {
7 | #[serde(alias = "minutely", alias = "MINUTELY")]
8 | Minutely,
9 | #[serde(alias = "hourly", alias = "HOURLY")]
10 | Hourly,
11 | #[serde(alias = "daily", alias = "DAILY")]
12 | Daily,
13 | #[serde(alias = "never", alias = "NEVER")]
14 | Never,
15 | }
16 |
17 | impl From<LogRotationKind> for Rotation {
18 | fn from(value: LogRotationKind) -> Self {
19 | match value {
20 | LogRotationKind::Minutely => Rotation::MINUTELY,
21 | LogRotationKind::Hourly => Rotation::HOURLY,
22 | LogRotationKind::Daily => Rotation::DAILY,
23 | LogRotationKind::Never => Rotation::NEVER,
24 | }
25 | }
26 | }
27 |
28 | #[cfg(test)]
29 | mod tests {
30 | use super::LogRotationKind;
31 | use rstest::rstest;
32 | use tracing_appender::rolling::Rotation;
33 |
34 | #[rstest]
35 | #[case(LogRotationKind::Minutely, Rotation::MINUTELY)]
36 | #[case(LogRotationKind::Hourly, Rotation::HOURLY)]
37 | #[case(LogRotationKind::Daily, Rotation::DAILY)]
38 | #[case(LogRotationKind::Never, Rotation::NEVER)]
39 | fn it_maps_to_rotation_correctly(
40 | #[case] log_rotation_kind: LogRotationKind,
41 | #[case] expected: Rotation,
42 | ) {
43 | let actual: Rotation = log_rotation_kind.into();
44 | assert_eq!(expected, actual);
45 | }
46 | }
47 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/testdata/minimal_supergraph.graphql:
--------------------------------------------------------------------------------
```graphql
1 | schema
2 | @link(url: "https://specs.apollo.dev/link/v1.0")
3 | @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
4 | query: Query
5 | }
6 |
7 | directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
8 |
9 | directive @join__field(
10 | graph: join__Graph
11 | requires: join__FieldSet
12 | provides: join__FieldSet
13 | type: String
14 | external: Boolean
15 | override: String
16 | usedOverridden: Boolean
17 | ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
18 |
19 | directive @join__graph(name: String!, url: String!) on ENUM_VALUE
20 |
21 | directive @join__type(
22 | graph: join__Graph!
23 | key: join__FieldSet
24 | extension: Boolean! = false
25 | resolvable: Boolean! = true
26 | isInterfaceObject: Boolean! = false
27 | ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
28 |
29 | directive @join__unionMember(
30 | graph: join__Graph!
31 | member: String!
32 | ) repeatable on UNION
33 |
34 | directive @link(
35 | url: String
36 | as: String
37 | for: link__Purpose
38 | import: [link__Import]
39 | ) repeatable on SCHEMA
40 |
41 | directive @join__implements(
42 | graph: join__Graph!
43 | interface: String!
44 | ) repeatable on OBJECT | INTERFACE
45 |
46 | scalar join__FieldSet
47 | scalar link__Import
48 |
49 | enum join__Graph {
50 | SUBGRAPH_A
51 | @join__graph(
52 | name: "subgraph-a"
53 | url: "http://graphql.subgraph-a.svc.cluster.local:4000"
54 | )
55 | }
56 |
57 | enum link__Purpose {
58 | SECURITY
59 | EXECUTION
60 | }
61 |
62 | type Query @join__type(graph: SUBGRAPH_A) {
63 | me: String @join__field(graph: SUBGRAPH_A)
64 | }
65 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/json_schema.rs:
--------------------------------------------------------------------------------
```rust
1 | /// Macro to generate a JSON schema from a type
2 | #[macro_export]
3 | macro_rules! schema_from_type {
4 | ($type:ty) => {{
5 | // Use Draft-07 for compatibility with MCP clients like VSCode/Copilot that don't support newer drafts.
6 | // See: https://github.com/microsoft/vscode/issues/251315
7 | let settings = schemars::generate::SchemaSettings::draft07();
8 | let generator = settings.into_generator();
9 | let schema = generator.into_root_schema_for::<$type>();
10 | match serde_json::to_value(schema) {
11 | Ok(Value::Object(schema)) => schema,
12 | _ => panic!("Failed to generate schema for {}", stringify!($type)),
13 | }
14 | }};
15 | }
16 |
17 | #[cfg(test)]
18 | mod tests {
19 | use schemars::JsonSchema;
20 | use serde::Deserialize;
21 | use serde_json::Value;
22 |
23 | #[derive(JsonSchema, Deserialize)]
24 | struct TestInput {
25 | #[allow(dead_code)]
26 | field: String,
27 | }
28 |
29 | #[test]
30 | fn test_schema_from_type() {
31 | let schema = schema_from_type!(TestInput);
32 |
33 | assert_eq!(
34 | serde_json::to_value(&schema).unwrap(),
35 | serde_json::json!({
36 | "$schema": "http://json-schema.org/draft-07/schema#",
37 | "title": "TestInput",
38 | "type": "object",
39 | "properties": {
40 | "field": {
41 | "type": "string"
42 | }
43 | },
44 | "required": ["field"]
45 | })
46 | );
47 | }
48 | }
49 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/endpoint.rs:
--------------------------------------------------------------------------------
```rust
1 | //! Endpoint newtype
2 | //!
3 | //! This module defines a simple newtype around a Url for demarking a GraphQL
4 | //! endpoint. This allows overlaying validation and default behaviour on top
5 | //! of the wrapped URL.
6 |
7 | use std::ops::Deref;
8 |
9 | use serde::Deserialize;
10 | use url::Url;
11 |
12 | /// A GraphQL endpoint
13 | #[derive(Debug)]
14 | pub struct Endpoint(Url);
15 |
16 | impl Endpoint {
17 | /// Unwrap the endpoint into its inner URL
18 | pub fn into_inner(self) -> Url {
19 | self.0
20 | }
21 | }
22 |
23 | impl Default for Endpoint {
24 | fn default() -> Self {
25 | Self(defaults::endpoint())
26 | }
27 | }
28 |
29 | impl<'de> Deserialize<'de> for Endpoint {
30 | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31 | where
32 | D: serde::Deserializer<'de>,
33 | {
34 | // This is a simple wrapper around URL, so we just use its deserializer
35 | let url = Url::deserialize(deserializer)?;
36 | Ok(Self(url))
37 | }
38 | }
39 |
40 | impl Deref for Endpoint {
41 | type Target = Url;
42 |
43 | fn deref(&self) -> &Self::Target {
44 | &self.0
45 | }
46 | }
47 |
48 | mod defaults {
49 | use url::Url;
50 |
51 | pub(super) fn endpoint() -> Url {
52 | // SAFETY: This should always parse correctly and is considered a breaking
53 | // error otherwise. It is also explicitly tested in [test::default_endpoint_parses_correctly]
54 | #[allow(clippy::unwrap_used)]
55 | Url::parse("http://127.0.0.1:4000").unwrap()
56 | }
57 |
58 | #[cfg(test)]
59 | mod test {
60 | use super::endpoint;
61 |
62 | #[test]
63 | fn default_endpoint_parses_correctly() {
64 | endpoint();
65 | }
66 | }
67 | }
68 |
```
--------------------------------------------------------------------------------
/docs/source/auth.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Authorization with Apollo MCP Server
3 | redirectFrom:
4 | - /apollo-mcp-server/guides/auth
5 | ---
6 |
7 | The Apollo MCP server supports authorizing clients (e.g., LLMs) in accordance with [the MCP specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization).
8 |
9 | The current implementation passes through OAuth tokens from MCP clients directly to upstream GraphQL APIs. You can read more about [security considerations](/apollo-mcp-server/limitations#oauth-token-passthrough) when using this feature.
10 |
11 | ## Implement authorization with Apollo MCP Server
12 |
13 | To implement authorization, you need an [OAuth 2.1-compliant](https://oauth.net/2.1/) Identity Provider (for example, your own in-house IdP or a third-party IdP such as Auth0, Okta, or Keycloak). You need the following values from your IdP:
14 |
15 | - **URL**: The base URL of your Identity Provider, which is used to validate the JSON Web Tokens (JWTs) issued by it.
16 | - **Audience**: Identifies the intended recipient of the token, typically a resource server or API. Represented by the `aud` claim in the JWT.
17 | - **Scopes**: The scopes that the client will request. These scopes define the permissions granted to the client when it accesses the API.
18 |
19 | Then, you [configure the MCP server with `auth` settings](/apollo-mcp-server/config-file#auth) and the [GraphOS Router for JWT authentication](/graphos/routing/security/jwt) using those IdP values.
20 |
21 | For an example of how to configure Apollo MCP Server with Auth0, see [Authorization with Auth0](/apollo-mcp-server/guides/auth-auth0).
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/telemetry_attributes.rs:
--------------------------------------------------------------------------------
```rust
1 | use crate::generated::telemetry::{ALL_ATTRS, TelemetryAttribute};
2 | use opentelemetry::Key;
3 | use std::collections::HashSet;
4 |
5 | impl TelemetryAttribute {
6 | pub const fn to_key(self) -> Key {
7 | match self {
8 | TelemetryAttribute::ToolName => {
9 | Key::from_static_str(TelemetryAttribute::ToolName.as_str())
10 | }
11 | TelemetryAttribute::OperationId => {
12 | Key::from_static_str(TelemetryAttribute::OperationId.as_str())
13 | }
14 | TelemetryAttribute::OperationSource => {
15 | Key::from_static_str(TelemetryAttribute::OperationSource.as_str())
16 | }
17 | TelemetryAttribute::Success => {
18 | Key::from_static_str(TelemetryAttribute::Success.as_str())
19 | }
20 | TelemetryAttribute::RequestId => {
21 | Key::from_static_str(TelemetryAttribute::RequestId.as_str())
22 | }
23 | TelemetryAttribute::RawOperation => {
24 | Key::from_static_str(TelemetryAttribute::RawOperation.as_str())
25 | }
26 | TelemetryAttribute::ClientName => {
27 | Key::from_static_str(TelemetryAttribute::ClientName.as_str())
28 | }
29 | TelemetryAttribute::ClientVersion => {
30 | Key::from_static_str(TelemetryAttribute::ClientVersion.as_str())
31 | }
32 | }
33 | }
34 |
35 | pub fn included_attributes(omitted: HashSet<TelemetryAttribute>) -> Vec<TelemetryAttribute> {
36 | ALL_ATTRS
37 | .iter()
38 | .copied()
39 | .filter(|a| !omitted.contains(a))
40 | .collect()
41 | }
42 | }
43 |
```
--------------------------------------------------------------------------------
/.github/renovate.json5:
--------------------------------------------------------------------------------
```
1 | {
2 | // Allow for intellisense in editors
3 | $schema: "https://docs.renovatebot.com/renovate-schema.json",
4 |
5 | // List of rules to apply
6 | extends: [
7 | // Recommended best practices from renovate itself
8 | // See: https://docs.renovatebot.com/upgrade-best-practices/#whats-in-the-configbest-practices-preset
9 | "config:best-practices",
10 |
11 | // Apply our own internal best practices
12 | // See: https://github.com/apollographql/apollo-mcp-server/commits/main/.github/renovate.json5
13 | "github>apollographql/renovate-config-apollo-open-source:default.json5",
14 |
15 | // Update to the latest rust stable version as it releases.
16 | // See: https://github.com/Turbo87/renovate-config/blob/master/rust/updateToolchain.json
17 | "github>Turbo87/renovate-config//rust/updateToolchain",
18 | ],
19 |
20 | // Globally disable all automatic update PRs from renovate
21 | packageRules: [
22 | {
23 | enabled: false,
24 | matchPackageNames: ["*"],
25 | },
26 | ],
27 |
28 | // Automating Nix upgrades is currently in beta and opt-in only.
29 | // https://docs.renovatebot.com/modules/manager/nix/
30 | nix: {
31 | enabled: true,
32 | },
33 |
34 | // Globally enable vulnerability alerts
35 | //
36 | // Note: This needs extra configuration at the repository level, which is described in the link
37 | // below.
38 | //
39 | // See: https://docs.renovatebot.com/configuration-options/#vulnerabilityalerts
40 | vulnerabilityAlerts: {
41 | enabled: true,
42 | },
43 |
44 | // Disable automatically updating lock files to latest versions once a week.
45 | //
46 | // See: https://docs.renovatebot.com/configuration-options/#lockfilemaintenance
47 | lockFileMaintenance: {
48 | enabled: false,
49 | },
50 | }
51 |
```
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
```toml
1 | [workspace]
2 | resolver = "2"
3 | members = [
4 | "crates/apollo-mcp-server",
5 | "crates/apollo-mcp-registry",
6 | "crates/apollo-schema-index",
7 | ]
8 |
9 | [workspace.package]
10 | authors = ["Apollo <[email protected]>"]
11 | edition = "2024"
12 | license-file = "LICENSE"
13 | repository = "https://github.com/apollographql/apollo-mcp-server"
14 | rust-version = "1.89.0"
15 | version = "1.1.1"
16 |
17 | [workspace.dependencies]
18 | apollo-compiler = "1.27.0"
19 | apollo-federation = "2.1.3"
20 | futures = { version = "0.3.31", features = ["thread-pool"] }
21 | insta = { version = "1.43.1", features = [
22 | "json",
23 | "redactions",
24 | "yaml",
25 | "glob",
26 | ] }
27 | reqwest = { version = "0.12.15", default-features = false, features = [
28 | "gzip",
29 | "json",
30 | "native-tls-vendored",
31 | ] }
32 | rstest = "0.25.0"
33 | secrecy = { version = "0.10.3", features = ["serde"] }
34 | serde = { version = "1.0.219", features = ["derive"] }
35 | serde_json = "1.0.140"
36 | thiserror = "2.0.12"
37 | tokio = { version = "1.45.0", features = [
38 | "fs",
39 | "io-std",
40 | "macros",
41 | "net",
42 | "rt",
43 | "rt-multi-thread",
44 | "signal",
45 | "sync",
46 | "time",
47 | ] }
48 | tokio-stream = "0.1"
49 | tracing = "0.1.41"
50 | tracing-core = "0.1.33"
51 | tracing-subscriber = { version = "0.3.19", features = ["json"] }
52 | url = { version = "2.4", features = ["serde"] }
53 |
54 | [workspace.metadata]
55 | crane.name = "apollo-mcp"
56 |
57 | # This allows usage of coverage(off) attribute without causing a linting error.
58 | # This attribute doesn't work in stable Rust yet and can be removed whenever it does.
59 | # See https://github.com/apollographql/apollo-mcp-server/pull/372
60 | [workspace.lints.rust]
61 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
62 |
63 | [workspace.lints.clippy]
64 | exit = "deny"
65 | expect_used = "deny"
66 | indexing_slicing = "deny"
67 | unwrap_used = "deny"
68 | panic = "deny"
69 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/platform_api.rs:
--------------------------------------------------------------------------------
```rust
1 | use secrecy::SecretString;
2 | use std::fmt::Debug;
3 | use std::time::Duration;
4 | use url::Url;
5 |
6 | pub mod operation_collections;
7 |
8 | const DEFAULT_PLATFORM_API: &str = "https://graphql.api.apollographql.com/api/graphql";
9 |
10 | /// Configuration for polling Apollo Uplink.
11 | #[derive(Clone, Debug)]
12 | pub struct PlatformApiConfig {
13 | /// The Apollo key: `<YOUR_GRAPH_API_KEY>`
14 | pub apollo_key: SecretString,
15 |
16 | /// The duration between polling
17 | pub poll_interval: Duration,
18 |
19 | /// The HTTP client timeout for each poll
20 | pub timeout: Duration,
21 |
22 | /// The URL of the Apollo registry
23 | pub registry_url: Url,
24 | }
25 |
26 | impl PlatformApiConfig {
27 | /// Creates a new `PlatformApiConfig` with the given Apollo key and default values for other fields.
28 | pub fn new(
29 | apollo_key: SecretString,
30 | poll_interval: Duration,
31 | timeout: Duration,
32 | registry_url: Option<Url>,
33 | ) -> Self {
34 | Self {
35 | apollo_key,
36 | poll_interval,
37 | timeout,
38 | #[allow(clippy::expect_used)]
39 | registry_url: registry_url
40 | .unwrap_or(Url::parse(DEFAULT_PLATFORM_API).expect("default URL should be valid")),
41 | }
42 | }
43 | }
44 |
45 | #[cfg(test)]
46 | mod test {
47 | use super::*;
48 | use secrecy::{ExposeSecret, SecretString};
49 | use std::time::Duration;
50 |
51 | #[test]
52 | fn test_platform_api_config_with_none_endpoints() {
53 | let config = PlatformApiConfig::new(
54 | SecretString::from("test_apollo_key"),
55 | Duration::from_secs(10),
56 | Duration::from_secs(5),
57 | None,
58 | );
59 | assert_eq!(config.apollo_key.expose_secret(), "test_apollo_key");
60 | assert_eq!(config.poll_interval, Duration::from_secs(10));
61 | assert_eq!(config.timeout, Duration::from_secs(5));
62 | assert_eq!(config.registry_url.to_string(), DEFAULT_PLATFORM_API);
63 | }
64 | }
65 |
```
--------------------------------------------------------------------------------
/docs/source/best-practices.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Best Practices
3 | subtitle: Guidelines for using Apollo MCP Server
4 | ---
5 |
6 | ## Writing your schema for efficient MCP tools
7 |
8 | The schema is required for:
9 |
10 | - **Tool Descriptions**: The schema provides type information used to generate tool descriptions. You can override these descriptions by adding comments to your operation files.
11 | - **Input Validation**: The schema is used to translate GraphQL input types into JSON Schema, ensuring that AI models provide correctly formatted inputs.
12 | - **Introspection Support**: If you enable the `introspection` option, the schema is used to provide information about available types and operations to AI models.
13 |
14 | ## Use contract variants to control AI access to graphs
15 |
16 | GraphOS [contract variants](/graphos/platform/schema-management/delivery/contracts/overview) let you deliver different subsets of your graph to different consumers.
17 |
18 | When running Apollo MCP Server with GraphOS, use contract variants whenever possible. This allows you to control which parts of your graph are accessible to AI by exposing only the necessary subsets.
19 |
20 | In particular, we strongly recommend contract variants when using:
21 |
22 | - [GraphOS-managed persisted queries](/apollo-mcp-server/define-tools#from-graphos-managed-persisted-queries)
23 | - [Introspection](/apollo-mcp-server/define-tools#introspection-tools)
24 |
25 | ## Send client name header when using persisted queries
26 |
27 | If you register a persisted query with a specific client name instead of `null`, you must configure the MCP Server to send the necessary header indicating the client name to the router.
28 |
29 | Use [the `headers` option](/apollo-mcp-server/config-file#headers) when running the MCP Server to pass the header to the router. The default name of the header expected by the router is `apollographql-client-name`. To use a different header name, configure `telemetry.apollo.client_name_header` in router YAML configuration.
30 |
```
--------------------------------------------------------------------------------
/docs/source/health-checks.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Health Checks
3 | ---
4 |
5 |
6 | ## Health checks
7 |
8 | Apollo MCP Server provides health check endpoints for monitoring server health and readiness. This feature is useful for load balancers, container orchestrators, and monitoring systems.
9 |
10 | ### Configuration
11 |
12 | Health checks are only available when using the `streamable_http` transport and must be explicitly enabled:
13 |
14 | ```yaml title="Example health check configuration"
15 | transport:
16 | type: streamable_http
17 | address: 127.0.0.1
18 | port: 8000
19 | health_check:
20 | enabled: true
21 | path: /health
22 | readiness:
23 | allowed: 50
24 | interval:
25 | sampling: 10s
26 | unready: 30s
27 | ```
28 |
29 | ### Endpoints
30 |
31 | The health check provides different responses based on query parameters:
32 |
33 | | Endpoint | Description | Response |
34 | | :------------------ | :----------------- | :--------------------------------------------------------------- |
35 | | `GET /health` | Basic health check | Always returns `{"status": "UP"}` |
36 | | `GET /health?live` | Liveness check | Returns `{"status": "UP"}` if server is alive |
37 | | `GET /health?ready` | Readiness check | Returns `{"status": "UP"}` if server is ready to handle requests |
38 |
39 | ### Probes
40 |
41 | The server tracks failed requests and automatically marks itself as unready if too many failures occur within a sampling interval:
42 |
43 | - **Sampling interval**: How often the server checks the rejection count (default: 5 seconds)
44 | - **Allowed rejections**: Maximum failures allowed before becoming unready (default: 100)
45 | - **Recovery time**: How long to wait before attempting to recover (default: 2x sampling interval)
46 |
47 | When the server becomes unready:
48 |
49 | - The `/health?ready` endpoint returns HTTP 503 with `{"status": "DOWN"}`
50 | - After the recovery period, the rejection counter resets and the server becomes ready again
51 |
52 | This allows external systems to automatically route traffic away from unhealthy servers and back when they recover.
53 |
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/local-operations/tool-tests.yaml:
--------------------------------------------------------------------------------
```yaml
1 | tools:
2 | expected_tool_list: ['introspect', 'execute', 'search', 'validate', 'SearchUpcomingLaunches', 'ExploreCelestialBodies', 'GetAstronautDetails', 'GetAstronautsCurrentlyInSpace']
3 |
4 | tests:
5 | - name: 'Introspection of launches query'
6 | tool: 'introspect'
7 | params:
8 | type_name: launches
9 | depth: 1
10 | expect:
11 | success: true
12 |
13 | - name: 'Search for launches query'
14 | tool: 'search'
15 | params:
16 | terms: ['launches']
17 | expect:
18 | success: true
19 | result:
20 | contains: 'launches(search: String, limit: Int = 5, offset: Int = 0): LaunchConnection'
21 |
22 | - name: 'Validate a valid launches query'
23 | tool: 'validate'
24 | params:
25 | operation: >
26 | query GetLaunches {
27 | launches {
28 | results {
29 | id
30 | name
31 | launchDesignator
32 | }
33 | }
34 | }
35 | expect:
36 | success: true
37 | result:
38 | contains: 'Operation is valid'
39 |
40 | - name: 'Validates an invalid query'
41 | tool: 'validate'
42 | params:
43 | operation: >
44 | query { invalidField }
45 | expect:
46 | success: false
47 | error:
48 | contains: 'Error: type `Query` does not have a field `invalidField`'
49 |
50 | - name: 'Validates a launches query with an invalid field'
51 | tool: 'validate'
52 | params:
53 | operation: >
54 | query GetLaunches {
55 | launches {
56 | results {
57 | id
58 | invalid
59 | }
60 | }
61 | }
62 | expect:
63 | success: false
64 | error:
65 | contains: 'Error: type `Launch` does not have a field `invalid`'
66 |
67 | - name: 'Validates a launches query with an missing argument'
68 | tool: 'validate'
69 | params:
70 | operation: >
71 | query Agency {
72 | agency {
73 | id
74 | }
75 | }
76 | expect:
77 | success: false
78 | error:
79 | contains: 'Error: the required argument `Query.agency(id:)` is not provided'
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/pq-manifest/tool-tests.yaml:
--------------------------------------------------------------------------------
```yaml
1 | tools:
2 | expected_tool_list: ['introspect', 'execute', 'search', 'validate', 'SearchUpcomingLaunches', 'ExploreCelestialBodies', 'GetAstronautDetails', 'GetAstronautsCurrentlyInSpace']
3 |
4 | tests:
5 | - name: 'Introspection of launches query'
6 | tool: 'introspect'
7 | params:
8 | type_name: launches
9 | depth: 1
10 | expect:
11 | success: true
12 |
13 | - name: 'Search for launches query'
14 | tool: 'search'
15 | params:
16 | terms: ['launches']
17 | expect:
18 | success: true
19 | result:
20 | contains: 'launches(search: String, limit: Int = 5, offset: Int = 0): LaunchConnection'
21 |
22 | - name: 'Validate a valid launches query'
23 | tool: 'validate'
24 | params:
25 | operation: >
26 | query GetLaunches {
27 | launches {
28 | results {
29 | id
30 | name
31 | launchDesignator
32 | }
33 | }
34 | }
35 | expect:
36 | success: true
37 | result:
38 | contains: 'Operation is valid'
39 |
40 | - name: 'Validates an invalid query'
41 | tool: 'validate'
42 | params:
43 | operation: >
44 | query { invalidField }
45 | expect:
46 | success: false
47 | error:
48 | contains: 'Error: type `Query` does not have a field `invalidField`'
49 |
50 | - name: 'Validates a launches query with an invalid field'
51 | tool: 'validate'
52 | params:
53 | operation: >
54 | query GetLaunches {
55 | launches {
56 | results {
57 | id
58 | invalid
59 | }
60 | }
61 | }
62 | expect:
63 | success: false
64 | error:
65 | contains: 'Error: type `Launch` does not have a field `invalid`'
66 |
67 | - name: 'Validates a launches query with an missing argument'
68 | tool: 'validate'
69 | params:
70 | operation: >
71 | query Agency {
72 | agency {
73 | id
74 | }
75 | }
76 | expect:
77 | success: false
78 | error:
79 | contains: 'Error: the required argument `Query.agency(id:)` is not provided'
80 |
```
--------------------------------------------------------------------------------
/docs/source/limitations.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Limitations
3 | ---
4 |
5 |
6 | ## Known limitations
7 |
8 | ### OAuth token passthrough
9 |
10 | Apollo MCP Server currently passes through OAuth tokens received from MCP clients directly to upstream GraphQL APIs.
11 |
12 | #### Rationale
13 |
14 | The decision to pass through tokens stems from practical enterprise requirements that may conflict with the MCP specification. For example:
15 |
16 | - **Multi-user scenarios**: Enterprise GraphQL APIs often require direct user identity to perform granular access control and tenant isolation.
17 | - **Existing enterprise patterns**: Many organizations have GraphQL APIs that depend on the original Authorization header to identify users and apply existing identity-based access controls.
18 | - **Multi-tenant applications**: Upstream APIs frequently need to read the Authorization header to identify the tenant and apply appropriate data filtering.
19 | - **User context propagation**: The MCP specification lacks clear guidance on how user/session/identity information should reach upstream APIs when they need to perform their own authorization logic.
20 |
21 | #### Security implications
22 |
23 | - Token passthrough can lead to confused deputy vulnerabilities.
24 | - Upstream APIs might incorrectly trust tokens as if they were validated by the MCP server.
25 | - Tokens intended for the MCP server audience might be inappropriately used with different services.
26 | - However, if upstream APIs enforce proper audience (`aud` claim) validation, they should reject inappropriately scoped tokens.
27 |
28 | #### Recommended workaround
29 |
30 | - Use the MCP server only with GraphQL APIs that accept the same OAuth tokens and audiences
31 | - Ensure your OAuth authorization server issues tokens with appropriate audience claims for both the MCP server and upstream APIs
32 | - Verify that your upstream APIs properly validate token audiences
33 | - Consider the security implications in your threat model, especially regarding OAuth trust boundaries
34 |
35 | #### Future plans
36 |
37 | We plan to address this limitation in a future release by implementing token exchange or separate authentication flows for upstream APIs, while still supporting the enterprise requirement for user identity propagation.
38 |
```
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "node",
6 | "request": "launch",
7 | "name": "Run apollo-mcp-server [Weather][Streamable HTTP]",
8 | "runtimeExecutable": "cargo",
9 | "runtimeArgs": [
10 | "run",
11 | "--bin",
12 | "apollo-mcp-server",
13 | "--",
14 | "graphql/weather/config.yaml"
15 | ],
16 | "cwd": "${workspaceFolder}",
17 | "console": "integratedTerminal",
18 | "env": {
19 | "RUST_BACKTRACE": "1"
20 | }
21 | },
22 | {
23 | "type": "lldb",
24 | "request": "launch",
25 | "name": "Debug apollo-mcp-server [Weather][Streamable HTTP]",
26 | "cargo": {
27 | "args": ["build", "--bin=apollo-mcp-server", "--lib"],
28 | "filter": {
29 | "name": "apollo-mcp-server",
30 | "kind": "bin"
31 | }
32 | },
33 | "args": ["graphql/weather/config.yaml"],
34 | "cwd": "${workspaceFolder}",
35 | "env": {
36 | "RUST_BACKTRACE": "1",
37 | "APOLLO_MCP_LOGGING__LEVEL": "debug"
38 | }
39 | },
40 | {
41 | "type": "node",
42 | "request": "launch",
43 | "name": "Run apollo-mcp-server [TheSpaceDevs][Streamable HTTP]",
44 | "runtimeExecutable": "cargo",
45 | "runtimeArgs": [
46 | "run",
47 | "--bin",
48 | "apollo-mcp-server",
49 | "--",
50 | "graphql/TheSpaceDevs/config.yaml"
51 | ],
52 | "cwd": "${workspaceFolder}",
53 | "console": "integratedTerminal",
54 | "env": {
55 | "RUST_BACKTRACE": "1"
56 | }
57 | },
58 | {
59 | "type": "lldb",
60 | "request": "launch",
61 | "name": "Debug apollo-mcp-server [TheSpaceDevs][Streamable HTTP]",
62 | "cargo": {
63 | "args": ["build", "--bin=apollo-mcp-server", "--lib"],
64 | "filter": {
65 | "name": "apollo-mcp-server",
66 | "kind": "bin"
67 | }
68 | },
69 | "args": ["graphql/TheSpaceDevs/config.yaml"],
70 | "cwd": "${workspaceFolder}",
71 | "env": {
72 | "RUST_BACKTRACE": "1",
73 | "APOLLO_MCP_LOGGING__LEVEL": "debug"
74 | }
75 | },
76 | {
77 | "type": "node",
78 | "request": "launch",
79 | "name": "Run mcp-inspector",
80 | "runtimeExecutable": "npx",
81 | "runtimeArgs": ["@modelcontextprotocol/inspector"],
82 | "cwd": "${workspaceFolder}",
83 | "console": "integratedTerminal"
84 | }
85 | ]
86 | }
87 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/introspection.rs:
--------------------------------------------------------------------------------
```rust
1 | use schemars::JsonSchema;
2 | use serde::Deserialize;
3 |
4 | /// Introspection configuration
5 | #[derive(Debug, Default, Deserialize, JsonSchema)]
6 | #[serde(default)]
7 | pub struct Introspection {
8 | /// Execution configuration for introspection
9 | pub execute: ExecuteConfig,
10 |
11 | /// Introspect configuration for allowing clients to run introspection
12 | pub introspect: IntrospectConfig,
13 |
14 | /// Search tool configuration
15 | pub search: SearchConfig,
16 |
17 | /// Validate configuration for checking operations before execution
18 | pub validate: ValidateConfig,
19 | }
20 |
21 | /// Execution-specific introspection configuration
22 | #[derive(Debug, Default, Deserialize, JsonSchema)]
23 | #[serde(default)]
24 | pub struct ExecuteConfig {
25 | /// Enable introspection for execution
26 | pub enabled: bool,
27 | }
28 |
29 | /// Introspect-specific introspection configuration
30 | #[derive(Debug, Default, Deserialize, JsonSchema)]
31 | #[serde(default)]
32 | pub struct IntrospectConfig {
33 | /// Enable introspection requests
34 | pub enabled: bool,
35 |
36 | /// Minify introspection results
37 | pub minify: bool,
38 | }
39 |
40 | /// Search tool configuration
41 | #[derive(Debug, Deserialize, JsonSchema)]
42 | #[serde(default)]
43 | pub struct SearchConfig {
44 | /// Enable search tool
45 | pub enabled: bool,
46 |
47 | /// The amount of memory used for indexing (in bytes)
48 | pub index_memory_bytes: usize,
49 |
50 | /// The depth of subtype information to include from matching types
51 | /// (1 is just the matching type, 2 is the matching type plus the types it references, etc.
52 | /// Defaults to 1.)
53 | pub leaf_depth: usize,
54 |
55 | /// Minify search results
56 | pub minify: bool,
57 | }
58 |
59 | impl Default for SearchConfig {
60 | fn default() -> Self {
61 | Self {
62 | enabled: false,
63 | index_memory_bytes: 50_000_000,
64 | leaf_depth: 1,
65 | minify: false,
66 | }
67 | }
68 | }
69 |
70 | /// Validation tool configuration
71 | #[derive(Debug, Default, Deserialize, JsonSchema)]
72 | #[serde(default)]
73 | pub struct ValidateConfig {
74 | /// Enable validation tool
75 | pub enabled: bool,
76 | }
77 |
78 | impl Introspection {
79 | /// Check if any introspection tools are enabled
80 | pub fn any_enabled(&self) -> bool {
81 | self.execute.enabled | self.introspect.enabled | self.search.enabled | self.validate.enabled
82 | }
83 | }
84 |
```
--------------------------------------------------------------------------------
/docs/source/debugging.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Debugging the MCP Server
3 | ---
4 |
5 |
6 | ## Debugging with MCP Inspector
7 |
8 | [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) is a debugging tool for MCP servers.
9 |
10 | ### Debug locally over stdio transport
11 |
12 | You can inspect a local Apollo MCP Server by running it with MCP Inspector.
13 |
14 | 1. Run the MCP Server with Inspector:
15 |
16 | ```yaml title="Example config for debugging over stdio"
17 | operations:
18 | source: local
19 | paths:
20 | - <absolute path to this git repo>/graphql/weather/operations/
21 | schema:
22 | source: local
23 | path: <absolute path to this git repo>/graphql/weather/api.graphql
24 | transport:
25 | type: stdio
26 | ```
27 |
28 | ```sh
29 | npx @modelcontextprotocol/inspector \
30 | target/debug/apollo-mcp-server <path to the preceding config>
31 | ```
32 |
33 | <ExpansionPanel title="Example output">
34 |
35 | ```sh showLineNumbers=false disableCopy=true
36 | Starting MCP inspector...
37 | ⚙️ Proxy server listening on port 6277
38 | 🔍 MCP Inspector is up and running at http://127.0.0.1:6274 🚀
39 | ```
40 |
41 | </ExpansionPanel>
42 |
43 | 1. In a browser, go to the URL returned by Inspector, then click **Connect** and **List Tools**. You should see the tools for the operations you provided.
44 |
45 | ### Debug over the Streamable HTTP transport
46 |
47 | When running the MCP Server over the Streamable HTTP transport, you can run MCP Inspector as follows.
48 |
49 | 1. Start the MCP Server in Streamable HTTP mode:
50 |
51 | <Tip>
52 |
53 | You can also deploy the server as a container using the instructions in [Deploying a Container](#deploying-a-container).
54 |
55 | </Tip>
56 |
57 | ```yaml title="Example config for running in Streamable HTTP"
58 | operations:
59 | source: local
60 | paths:
61 | - <absolute path to this git repo>/graphql/weather/operations/
62 | schema:
63 | source: local
64 | path: <absolute path to this git repo>/graphql/weather/api.graphql
65 | transport:
66 | type: streamable_http
67 | address: 127.0.0.1
68 | port: 8000
69 | ```
70 |
71 | ```sh
72 | target/debug/apollo-mcp-server <path to the above config>
73 | ```
74 |
75 | 1. Start the MCP Inspector:
76 |
77 | ```sh
78 | npx @modelcontextprotocol/inspector
79 | ```
80 |
81 | 1. In a browser, go to the URL returned by Inspector, then fill in the details:
82 |
83 | - **Transport Type**: Select `Streamable HTTP`
84 | - **URL**: Enter `http://127.0.0.1:8000/mcp`, where the port must match the `transport.port` option
85 |
86 | 1. Click **Connect** and **List Tools**. You should see the tools for the operations you provided.
87 |
```
--------------------------------------------------------------------------------
/.github/workflows/verify-changeset.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Verify Changeset
2 | on:
3 | pull_request:
4 | types: [opened, reopened, synchronize, ready_for_review]
5 | branches-ignore:
6 | - main
7 | - release/**
8 | - conflict/*
9 | - sync/*
10 | paths-ignore:
11 | - '.github/**'
12 | - '.cargo/**'
13 | - '.direnv/**'
14 | - '.vscode/**'
15 | - 'docs/**'
16 | - 'Cargo.*'
17 | - 'crates/**/Cargo.*'
18 | - '*.md'
19 | workflow_dispatch:
20 |
21 | jobs:
22 | verify-changeset:
23 | if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-changeset') && !startsWith(github.head_ref, 'sync/') && !startsWith(github.head_ref, 'conflict/') && !github.event.pull_request.draft }}
24 | name: Verify
25 | runs-on: ubuntu-24.04
26 | permissions:
27 | pull-requests: write
28 | contents: read
29 | steps:
30 | - name: Verify changeset included
31 | uses: actions/github-script@v7
32 | with:
33 | script: |
34 | const dir = '.changesets/';
35 | const pr = context.payload.pull_request;
36 | const files = await github.paginate(
37 | github.rest.pulls.listFiles,
38 | { owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, per_page: 100 }
39 | );
40 | const ok = files.some(f =>
41 | f.filename.startsWith(dir) &&
42 | ['added','modified','renamed'].includes(f.status)
43 | );
44 | if (!ok) {
45 | core.setFailed(`No changeset added to ${dir}.`);
46 | } else {
47 | core.info(`Changeset found under ${dir}.`);
48 | }
49 | core.setOutput('ok', ok ? 'true' : 'false');
50 | - name: Add changeset missing comment on failure
51 | uses: actions/github-script@v7
52 | if: failure()
53 | with:
54 | script: |
55 | const pr = context.payload.pull_request;
56 | await github.rest.issues.createComment({
57 | owner: context.repo.owner,
58 | repo: context.repo.repo,
59 | issue_number: pr.number,
60 | body: [
61 | "❌ **Changeset file missing for PR**",
62 | "",
63 | "All changes should include an associated changeset file.",
64 | "Please refer to [README](https://github.com/apollographql/apollo-mcp-server/blob/main/.changesets/README.md) for more information on generating changesets."
65 | ].join("\n")
66 | });
67 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/operations/schema_walker/type.rs:
--------------------------------------------------------------------------------
```rust
1 | use apollo_compiler::{Schema as GraphQLSchema, ast::Type as GraphQLType};
2 | use schemars::{Schema as JSONSchema, json_schema};
3 | use serde_json::{Map, Value};
4 |
5 | use crate::custom_scalar_map::CustomScalarMap;
6 |
7 | use super::name::Name;
8 |
9 | pub(super) struct Type<'a> {
10 | /// The definition cache which contains full schemas for nested types
11 | pub(super) cache: &'a mut Map<String, Value>,
12 |
13 | /// Custom scalar map for supplementing information from the GraphQL schema
14 | pub(super) custom_scalar_map: Option<&'a CustomScalarMap>,
15 |
16 | /// The optional description of the type, from comments in the schema
17 | pub(super) description: &'a Option<String>,
18 |
19 | /// The original GraphQL schema with all type information
20 | pub(super) schema: &'a GraphQLSchema,
21 |
22 | /// The actual type to translate into a JSON schema
23 | pub(super) r#type: &'a GraphQLType,
24 | }
25 |
26 | impl From<Type<'_>> for JSONSchema {
27 | fn from(
28 | Type {
29 | cache,
30 | custom_scalar_map,
31 | description,
32 | schema,
33 | r#type,
34 | }: Type,
35 | ) -> Self {
36 | // JSON Schema assumes that all properties are nullable unless there is a
37 | // required field, so we treat cases the same here.
38 | match r#type {
39 | GraphQLType::List(list) | GraphQLType::NonNullList(list) => {
40 | let nested_schema: JSONSchema = Type {
41 | cache,
42 | custom_scalar_map,
43 | description,
44 | schema,
45 | r#type: list,
46 | }
47 | .into();
48 |
49 | // Arrays, however, do need to specify that fields can be null
50 | let nested_schema = if list.is_non_null() {
51 | nested_schema
52 | } else {
53 | json_schema!({"oneOf": [
54 | nested_schema,
55 | {"type": "null"},
56 | ]})
57 | };
58 |
59 | json_schema!({
60 | "type": "array",
61 | "items": nested_schema,
62 | })
63 | }
64 |
65 | GraphQLType::Named(name) | GraphQLType::NonNullNamed(name) => JSONSchema::from(Name {
66 | cache,
67 | custom_scalar_map,
68 | description,
69 | name,
70 | schema,
71 | }),
72 | }
73 | }
74 | }
75 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/platform_api/operation_collections/operation_collections.graphql:
--------------------------------------------------------------------------------
```graphql
1 | fragment OperationData on OperationCollectionEntry {
2 | name
3 | currentOperationRevision {
4 | body
5 | headers {
6 | name
7 | value
8 | }
9 | variables
10 | }
11 | }
12 |
13 | query OperationCollectionQuery($operationCollectionId: ID!) {
14 | operationCollection(id: $operationCollectionId) {
15 | __typename
16 | ... on OperationCollection {
17 | operations {
18 | lastUpdatedAt
19 | id
20 | ...OperationData
21 | }
22 | }
23 | ... on NotFoundError {
24 | message
25 | }
26 | ... on PermissionError {
27 | message
28 | }
29 | ... on ValidationError {
30 | message
31 | }
32 | }
33 | }
34 |
35 | query OperationCollectionPollingQuery($operationCollectionId: ID!) {
36 | operationCollection(id: $operationCollectionId) {
37 | __typename
38 | ... on OperationCollection {
39 | operations {
40 | lastUpdatedAt
41 | id
42 | }
43 | }
44 | ... on NotFoundError {
45 | message
46 | }
47 | ... on PermissionError {
48 | message
49 | }
50 | ... on ValidationError {
51 | message
52 | }
53 | }
54 | }
55 |
56 | query OperationCollectionDefaultQuery($graphRef: ID!) {
57 | variant(ref: $graphRef) {
58 | __typename
59 | ... on GraphVariant {
60 | mcpDefaultCollection {
61 | __typename
62 | ... on OperationCollection {
63 | operations {
64 | lastUpdatedAt
65 | id
66 | ...OperationData
67 | }
68 | }
69 | ... on PermissionError {
70 | message
71 | }
72 | }
73 | }
74 | ... on InvalidRefFormat {
75 | message
76 | }
77 | }
78 | }
79 |
80 | query OperationCollectionDefaultPollingQuery($graphRef: ID!) {
81 | variant(ref: $graphRef) {
82 | __typename
83 | ... on GraphVariant {
84 | mcpDefaultCollection {
85 | __typename
86 | ... on OperationCollection {
87 | operations {
88 | id
89 | lastUpdatedAt
90 | }
91 | }
92 | ... on PermissionError {
93 | message
94 | }
95 | }
96 | }
97 | ... on InvalidRefFormat {
98 | message
99 | }
100 | }
101 | }
102 |
103 | query OperationCollectionEntriesQuery($collectionEntryIds: [ID!]!) {
104 | operationCollectionEntries(collectionEntryIds: $collectionEntryIds) {
105 | id
106 | lastUpdatedAt
107 | ...OperationData
108 | }
109 | }
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/run_tests.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | usage() {
5 | cat <<'USAGE'
6 | Usage: run-tools.sh <test-dir>
7 |
8 | Runs:
9 | npx mcp-server-tester tools <test-dir>/tool-tests.yaml --server-config <test-dir>/apollo-mcp-server-config.json
10 |
11 | Notes:
12 | - <test-dir> is resolved relative to this script's directory (not the caller's cwd),
13 | so calling: foo/bar/run-tools.sh local-directory
14 | uses: foo/bar/local-directory/tool-tests.yaml
15 | - If ../../target/release/apollo-mcp-server (relative to this script) doesn't exist,
16 | it is built from the repo root (../../) with: cargo build --release
17 | USAGE
18 | exit 1
19 | }
20 |
21 | [[ "${1:-}" == "-h" || "${1:-}" == "--help" || $# -eq 0 ]] && usage
22 |
23 | RAW_DIR_ARG="${1%/}"
24 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
25 |
26 | # If absolute path, use it as-is; otherwise, resolve relative to the script dir.
27 | if [[ "$RAW_DIR_ARG" = /* ]]; then
28 | TEST_DIR="$RAW_DIR_ARG"
29 | else
30 | TEST_DIR="$(cd -P -- "$SCRIPT_DIR/$RAW_DIR_ARG" && pwd)"
31 | fi
32 |
33 | TEST_DIR="${1%/}" # strip trailing slash if present
34 | TESTS="$TEST_DIR/tool-tests.yaml"
35 | MCP_CONFIG="$TEST_DIR/config.yaml"
36 |
37 | # Sanity checks
38 | [[ -f "$TESTS" ]] || { echo "✗ Missing file: $TESTS"; exit 2; }
39 | [[ -f "$MCP_CONFIG" ]] || { echo "✗ Missing file: $MCP_CONFIG"; exit 2; }
40 |
41 | REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
42 | BIN_PATH="$REPO_ROOT/target/release/apollo-mcp-server"
43 |
44 | if [[ ! -x "$BIN_PATH" ]]; then
45 | echo "ℹ️ Binary not found at: $BIN_PATH"
46 | echo "➡️ Building release binary from: $REPO_ROOT"
47 | (cd "$REPO_ROOT" && cargo build --release)
48 |
49 | # Re-check after build
50 | if [[ ! -x "$BIN_PATH" ]]; then
51 | echo "✗ Build succeeded but binary not found/executable at: $BIN_PATH"
52 | exit 3
53 | fi
54 | fi
55 |
56 | # Template → generated server-config
57 | TEMPLATE_PATH="${SERVER_CONFIG_TEMPLATE:-"$SCRIPT_DIR/server-config.template.json"}"
58 | [[ -f "$TEMPLATE_PATH" ]] || { echo "✗ Missing server-config template: $TEMPLATE_PATH"; exit 4; }
59 |
60 | TMP_DIR="$(mktemp -d)"
61 | cleanup() { rm -rf "$TMP_DIR"; }
62 | trap cleanup EXIT INT TERM # cleanup before exiting
63 | GEN_CONFIG="$TMP_DIR/server-config.generated.json"
64 |
65 | # Safe replacement for <test-dir> with absolute path (handles /, &, and |)
66 | safe_dir="${TEST_DIR//\\/\\\\}"
67 | safe_dir="${safe_dir//&/\\&}"
68 | safe_dir="${safe_dir//|/\\|}"
69 |
70 | # Replace the literal token "<test-dir>" everywhere
71 | sed "s|<test-dir>|$safe_dir|g" "$TEMPLATE_PATH" > "$GEN_CONFIG"
72 |
73 | # Run the command
74 | npx -y [email protected] tools "$TESTS" --server-config "$GEN_CONFIG"
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/auth/networked_token_validator.rs:
--------------------------------------------------------------------------------
```rust
1 | use jwks::{Jwk, Jwks};
2 | use tracing::warn;
3 | use url::Url;
4 |
5 | use super::valid_token::ValidateToken;
6 |
7 | /// Implementation of the `ValidateToken` trait which fetches key information
8 | /// from the network.
9 | pub(super) struct NetworkedTokenValidator<'a> {
10 | audiences: &'a Vec<String>,
11 | upstreams: &'a Vec<Url>,
12 | }
13 |
14 | impl<'a> NetworkedTokenValidator<'a> {
15 | pub fn new(audiences: &'a Vec<String>, upstreams: &'a Vec<Url>) -> Self {
16 | Self {
17 | audiences,
18 | upstreams,
19 | }
20 | }
21 | }
22 |
23 | /// Constructs the OIDC discovery URL by appending the well-known path to the oauth server URL.
24 | fn build_oidc_url(oauth_server: &Url) -> Url {
25 | let mut discovery_url = oauth_server.clone();
26 | // This ensures Keycloak URLs like /auth/realms/<realm>/ work correctly.
27 | let current_path = discovery_url.path().trim_end_matches('/');
28 | discovery_url.set_path(&format!(
29 | "{current_path}/.well-known/oauth-authorization-server"
30 | ));
31 | discovery_url
32 | }
33 |
34 | impl ValidateToken for NetworkedTokenValidator<'_> {
35 | fn get_audiences(&self) -> &Vec<String> {
36 | self.audiences
37 | }
38 |
39 | fn get_servers(&self) -> &Vec<Url> {
40 | self.upstreams
41 | }
42 |
43 | async fn get_key(&self, server: &Url, key_id: &str) -> Option<Jwk> {
44 | let oidc_url = build_oidc_url(server);
45 |
46 | let jwks = Jwks::from_oidc_url(oidc_url)
47 | .await
48 | .inspect_err(|e| {
49 | warn!("could not fetch OIDC information from {server}: {e}");
50 | })
51 | .ok()?;
52 |
53 | jwks.keys.get(key_id).cloned()
54 | }
55 | }
56 |
57 | #[cfg(test)]
58 | mod tests {
59 | use super::*;
60 | use rstest::rstest;
61 |
62 | #[rstest]
63 | // Keycloak
64 | #[case(
65 | "https://sso.company.com/auth/realms/my-realm",
66 | "https://sso.company.com/auth/realms/my-realm/.well-known/oauth-authorization-server"
67 | )]
68 | #[case(
69 | "https://sso.company.com/auth/realms/my-realm/",
70 | "https://sso.company.com/auth/realms/my-realm/.well-known/oauth-authorization-server"
71 | )]
72 | // Auth0
73 | #[case(
74 | "https://dev-abc123.us.auth0.com",
75 | "https://dev-abc123.us.auth0.com/.well-known/oauth-authorization-server"
76 | )]
77 | // WorkOS
78 | #[case(
79 | "https://abb-123-staging.authkit.app/",
80 | "https://abb-123-staging.authkit.app/.well-known/oauth-authorization-server"
81 | )]
82 | fn test_build_oidc_discovery_url(#[case] input: &str, #[case] expected: &str) {
83 | let oauth_url = Url::parse(input).unwrap();
84 | let oidc_url = build_oidc_url(&oauth_url);
85 |
86 | assert_eq!(oidc_url.as_str(), expected);
87 | }
88 | }
89 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/telemetry/sampler.rs:
--------------------------------------------------------------------------------
```rust
1 | use schemars::JsonSchema;
2 | use serde::Deserialize;
3 |
4 | #[derive(Clone, Debug, Deserialize, JsonSchema)]
5 | #[serde(deny_unknown_fields, untagged)]
6 | pub(crate) enum SamplerOption {
7 | /// Sample a given fraction. Fractions >= 1 will always sample.
8 | RatioBased(f64),
9 | Always(Sampler),
10 | }
11 |
12 | #[derive(Clone, Debug, Deserialize, JsonSchema)]
13 | #[serde(deny_unknown_fields, rename_all = "snake_case")]
14 | pub(crate) enum Sampler {
15 | /// Always sample
16 | AlwaysOn,
17 | /// Never sample
18 | AlwaysOff,
19 | }
20 |
21 | impl From<Sampler> for opentelemetry_sdk::trace::Sampler {
22 | fn from(s: Sampler) -> Self {
23 | match s {
24 | Sampler::AlwaysOn => opentelemetry_sdk::trace::Sampler::AlwaysOn,
25 | Sampler::AlwaysOff => opentelemetry_sdk::trace::Sampler::AlwaysOff,
26 | }
27 | }
28 | }
29 |
30 | impl From<SamplerOption> for opentelemetry_sdk::trace::Sampler {
31 | fn from(s: SamplerOption) -> Self {
32 | match s {
33 | SamplerOption::Always(s) => s.into(),
34 | SamplerOption::RatioBased(ratio) => {
35 | opentelemetry_sdk::trace::Sampler::TraceIdRatioBased(ratio)
36 | }
37 | }
38 | }
39 | }
40 |
41 | impl Default for SamplerOption {
42 | fn default() -> Self {
43 | SamplerOption::Always(Sampler::AlwaysOn)
44 | }
45 | }
46 |
47 | #[cfg(test)]
48 | mod tests {
49 | use super::*;
50 |
51 | #[test]
52 | fn sampler_always_on_maps_to_otel_always_on() {
53 | assert!(matches!(
54 | Sampler::AlwaysOn.into(),
55 | opentelemetry_sdk::trace::Sampler::AlwaysOn
56 | ));
57 | }
58 |
59 | #[test]
60 | fn sampler_always_off_maps_to_otel_always_off() {
61 | assert!(matches!(
62 | Sampler::AlwaysOff.into(),
63 | opentelemetry_sdk::trace::Sampler::AlwaysOff
64 | ));
65 | }
66 |
67 | #[test]
68 | fn sampler_option_always_on_maps_to_otel_always_on() {
69 | assert!(matches!(
70 | SamplerOption::Always(Sampler::AlwaysOn).into(),
71 | opentelemetry_sdk::trace::Sampler::AlwaysOn
72 | ));
73 | }
74 |
75 | #[test]
76 | fn sampler_option_always_off_maps_to_otel_always_off() {
77 | assert!(matches!(
78 | SamplerOption::Always(Sampler::AlwaysOff).into(),
79 | opentelemetry_sdk::trace::Sampler::AlwaysOff
80 | ));
81 | }
82 |
83 | #[test]
84 | fn sampler_option_ratio_based_maps_to_otel_ratio_based_sampler() {
85 | assert!(matches!(
86 | SamplerOption::RatioBased(0.5).into(),
87 | opentelemetry_sdk::trace::Sampler::TraceIdRatioBased(0.5)
88 | ));
89 | }
90 |
91 | #[test]
92 | fn default_sampler_option_is_always_on() {
93 | assert!(matches!(
94 | SamplerOption::default(),
95 | SamplerOption::Always(Sampler::AlwaysOn)
96 | ));
97 | }
98 | }
99 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/event.rs:
--------------------------------------------------------------------------------
```rust
1 | use crate::operations::RawOperation;
2 | use apollo_mcp_registry::platform_api::operation_collections::error::CollectionError;
3 | use apollo_mcp_registry::uplink::schema::event::Event as SchemaEvent;
4 | use std::fmt::Debug;
5 | use std::fmt::Formatter;
6 | use std::fmt::Result;
7 | use std::io;
8 |
9 | /// MCP Server events
10 | pub enum Event {
11 | /// The schema has been updated
12 | SchemaUpdated(SchemaEvent),
13 |
14 | /// The operations have been updated
15 | OperationsUpdated(Vec<RawOperation>),
16 |
17 | /// An error occurred when loading operations
18 | OperationError(io::Error, Option<String>),
19 |
20 | /// An error occurred when loading operations from collection
21 | CollectionError(CollectionError),
22 |
23 | /// The server should gracefully shut down
24 | Shutdown,
25 | }
26 |
27 | impl Debug for Event {
28 | fn fmt(&self, f: &mut Formatter) -> Result {
29 | match self {
30 | Event::SchemaUpdated(event) => {
31 | write!(f, "SchemaUpdated({event:?})")
32 | }
33 | Event::OperationsUpdated(operations) => {
34 | write!(f, "OperationsChanged({operations:?})")
35 | }
36 | Event::OperationError(e, path) => {
37 | write!(f, "OperationError({e:?}, {path:?})")
38 | }
39 | Event::CollectionError(e) => {
40 | write!(f, "OperationError({e:?})")
41 | }
42 | Event::Shutdown => {
43 | write!(f, "Shutdown")
44 | }
45 | }
46 | }
47 | }
48 |
49 | #[cfg(test)]
50 | mod tests {
51 | use super::*;
52 |
53 | #[test]
54 | fn test_debug_event_schema_updated() {
55 | let event = Event::SchemaUpdated(SchemaEvent::NoMoreSchema);
56 | let output = format!("{:?}", event);
57 | assert_eq!(output, "SchemaUpdated(NoMoreSchema)");
58 | }
59 |
60 | #[test]
61 | fn test_debug_event_operations_updated() {
62 | let event = Event::OperationsUpdated(vec![]);
63 | let output = format!("{:?}", event);
64 | assert_eq!(output, "OperationsChanged([])");
65 | }
66 |
67 | #[test]
68 | fn test_debug_event_operation_error() {
69 | let event = Event::OperationError(std::io::Error::other("TEST"), None);
70 | let output = format!("{:?}", event);
71 | assert_eq!(
72 | output,
73 | r#"OperationError(Custom { kind: Other, error: "TEST" }, None)"#
74 | );
75 | }
76 |
77 | #[test]
78 | fn test_debug_event_collection_error() {
79 | let event = Event::CollectionError(CollectionError::Response("TEST".to_string()));
80 | let output = format!("{:?}", event);
81 | assert_eq!(output, r#"OperationError(Response("TEST"))"#);
82 | }
83 |
84 | #[test]
85 | fn test_debug_event_shutdown() {
86 | let event = Event::Shutdown;
87 | let output = format!("{:?}", event);
88 | assert_eq!(output, "Shutdown");
89 | }
90 | }
91 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/Cargo.toml:
--------------------------------------------------------------------------------
```toml
1 | [package]
2 | name = "apollo-mcp-server"
3 | authors.workspace = true
4 | edition.workspace = true
5 | license-file.workspace = true
6 | repository.workspace = true
7 | rust-version.workspace = true
8 | version.workspace = true
9 | build = "build.rs"
10 |
11 | default-run = "apollo-mcp-server"
12 |
13 | [dependencies]
14 | anyhow = "1.0.98"
15 | apollo-compiler.workspace = true
16 | apollo-federation.workspace = true
17 | apollo-mcp-registry = { path = "../apollo-mcp-registry" }
18 | apollo-schema-index = { path = "../apollo-schema-index" }
19 | axum = "0.8.4"
20 | axum-extra = { version = "0.10.1", features = ["typed-header"] }
21 | axum-otel-metrics = "0.12.0"
22 | axum-tracing-opentelemetry = "0.29.0"
23 | bon = "3.6.3"
24 | clap = { version = "4.5.36", features = ["derive", "env"] }
25 | figment = { version = "0.10.19", features = ["env", "yaml"] }
26 | futures.workspace = true
27 | headers = "0.4.1"
28 | http = "1.3.1"
29 | humantime-serde = "1.1.1"
30 | jsonschema = "0.33.0"
31 | jsonwebtoken = "9"
32 | jwks = "0.4.0"
33 | lz-str = "0.2.1"
34 | opentelemetry = "0.30.0"
35 | opentelemetry-appender-log = "0.30.0"
36 | opentelemetry-otlp = { version = "0.30.0", features = [
37 | "grpc-tonic",
38 | "tonic",
39 | "http-proto",
40 | "metrics",
41 | "trace",
42 | ] }
43 | opentelemetry-resource-detectors = "0.9.0"
44 | opentelemetry-semantic-conventions = "0.30.0"
45 | opentelemetry-stdout = "0.30.0"
46 | opentelemetry_sdk = { version = "0.30.0", features = [
47 | "spec_unstable_metrics_views",
48 | ] }
49 | regex = "1.11.1"
50 | reqwest-middleware = "0.4.2"
51 | reqwest-tracing = { version = "0.5.8", features = ["opentelemetry_0_30"] }
52 | reqwest.workspace = true
53 | rmcp = { version = "0.8", features = [
54 | "server",
55 | "transport-io",
56 | "transport-sse-server",
57 | "transport-streamable-http-server",
58 | ] }
59 | schemars = { version = "1.0.1", features = ["url2"] }
60 | serde.workspace = true
61 | serde_json.workspace = true
62 | thiserror.workspace = true
63 | tokio.workspace = true
64 | tokio-util = "0.7.15"
65 | tower-http = { version = "0.6.6", features = ["cors", "trace"] }
66 | tracing-appender = "0.2.3"
67 | tracing-core.workspace = true
68 | tracing-opentelemetry = "0.31.0"
69 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
70 | tracing.workspace = true
71 | url.workspace = true
72 | async-trait = "0.1.89"
73 |
74 | [dev-dependencies]
75 | chrono = { version = "0.4.41", default-features = false, features = ["now"] }
76 | figment = { version = "0.10.19", features = ["test"] }
77 | insta.workspace = true
78 | mockito = "1.7.0"
79 | opentelemetry_sdk = { version = "0.30.0", features = ["testing"] }
80 | rstest.workspace = true
81 | tokio.workspace = true
82 | tower = "0.5.2"
83 | tracing-test = "0.2.5"
84 |
85 | [build-dependencies]
86 | cruet = "0.15.0"
87 | prettyplease = "0.2.37"
88 | quote = "1.0.40"
89 | serde.workspace = true
90 | syn = "2.0.106"
91 | toml = "0.9.5"
92 |
93 | [lints]
94 | workspace = true
95 |
96 | [[bin]]
97 | name = "apollo-mcp-server"
98 | path = "src/main.rs"
99 |
100 | [[bin]]
101 | name = "config-schema"
102 | path = "src/config_schema.rs"
103 | test = false
104 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/uplink/uplink.graphql:
--------------------------------------------------------------------------------
```graphql
1 | """
2 | Schema for requests to Apollo Uplink
3 | """
4 |
5 | type Query {
6 | """
7 | Fetch schema through router configuration
8 | """
9 | routerConfig(
10 | """
11 | The reference to a graph variant, like `engine@prod` or `engine` (i.e. `engine@current`).
12 | """
13 | ref: String!,
14 |
15 | """
16 | the API key to authenticate with
17 | """
18 | apiKey: String!,
19 |
20 | """
21 | When specified and the result is not newer, `Unchanged` is returned rather than `RouterConfigResult`.
22 | """
23 | ifAfterId: ID
24 | ): RouterConfigResponse!
25 |
26 | """
27 | Fetch persisted queries
28 | """
29 | persistedQueries(
30 | """
31 | The reference to a graph variant, like `engine@prod` or `engine` (i.e. `engine@current`).
32 | """
33 | ref: String!
34 |
35 | """
36 | the API key to authenticate with
37 | """
38 | apiKey: String!
39 |
40 | """
41 | When specified and the result is not newer, `Unchanged` is returned rather than `PersistedQueriesResult`.
42 | """
43 | ifAfterId: ID
44 | ): PersistedQueriesResponse!
45 | }
46 |
47 | union RouterConfigResponse = RouterConfigResult | Unchanged | FetchError
48 |
49 | type RouterConfigResult {
50 | "Variant-unique identifier."
51 | id: ID!
52 | "The configuration as core schema."
53 | supergraphSDL: String!
54 | "Messages that should be reported back to the operators of this router, eg through logs and/or monitoring."
55 | messages: [Message!]!
56 | "Minimum delay before the next fetch should occur, in seconds."
57 | minDelaySeconds: Float!
58 | }
59 |
60 | type Message {
61 | level: MessageLevel!
62 | body: String!
63 | }
64 |
65 | enum MessageLevel {
66 | ERROR
67 | WARN
68 | INFO
69 | }
70 |
71 | union PersistedQueriesResponse = PersistedQueriesResult | Unchanged | FetchError
72 |
73 | type PersistedQueriesResult {
74 | """
75 | Uniquely identifies this version. Must be passed via ifAfterId for incremental updates.
76 | """
77 | id: ID!
78 |
79 | """
80 | Minimum seconds to wait before checking again on 'unchanged'
81 | """
82 | minDelaySeconds: Float!
83 |
84 | """
85 | Chunks of operations
86 | """
87 | chunks: [PersistedQueriesResultChunks!]
88 | }
89 |
90 | """
91 | A sublist of the persisted query result which can be fetched directly from a content-addressed storage
92 | """
93 | type PersistedQueriesResultChunks {
94 | """
95 | Chunk ID
96 | """
97 | id: ID!
98 |
99 | """
100 | Locations to find the operations from
101 | """
102 | urls: [String!]!
103 | }
104 |
105 | type Unchanged {
106 | """
107 | Uniquely identifies this version. Must be passed via ifAfterId for subsequent checks.
108 | """
109 | id: ID!
110 |
111 | """
112 | Minimum seconds to wait before checking again
113 | """
114 | minDelaySeconds: Float!
115 | }
116 |
117 | enum FetchErrorCode {
118 | AUTHENTICATION_FAILED
119 | ACCESS_DENIED
120 | UNKNOWN_REF
121 | RETRY_LATER
122 | NOT_IMPLEMENTED_ON_THIS_INSTANCE
123 | }
124 |
125 | type FetchError {
126 | code: FetchErrorCode!
127 | message: String!
128 | }
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/runtime/operation_source.rs:
--------------------------------------------------------------------------------
```rust
1 | use std::path::PathBuf;
2 |
3 | use schemars::JsonSchema;
4 | use serde::Deserialize;
5 |
6 | /// Source for loaded operations
7 | #[derive(Debug, Default, Deserialize, JsonSchema)]
8 | #[serde(tag = "source", rename_all = "snake_case")]
9 | pub enum OperationSource {
10 | /// Load operations from a GraphOS collection
11 | Collection {
12 | #[schemars(with = "String")]
13 | id: IdOrDefault,
14 | },
15 |
16 | /// Infer where to load operations based on other configuration options.
17 | ///
18 | /// Note: This setting tries to load the operations from introspection, if enabled
19 | /// or from the default operation collection when APOLLO_GRAPH_REF is set.
20 | #[default]
21 | Infer,
22 |
23 | /// Load operations by introspecting the schema
24 | ///
25 | /// Note: Requires introspection to be enabled
26 | Introspect,
27 |
28 | /// Load operations from local GraphQL files / folders
29 | Local { paths: Vec<PathBuf> },
30 |
31 | /// Load operations from a persisted queries manifest file
32 | Manifest { path: PathBuf },
33 |
34 | /// Load operations from uplink manifest
35 | Uplink,
36 | }
37 |
38 | /// Either a custom ID or the default variant
39 | #[derive(Debug, PartialEq, Eq)]
40 | pub enum IdOrDefault {
41 | /// The default tools for the variant (requires APOLLO_KEY)
42 | Default,
43 |
44 | /// The specific collection ID
45 | Id(String),
46 | }
47 |
48 | impl<'de> Deserialize<'de> for IdOrDefault {
49 | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
50 | where
51 | D: serde::Deserializer<'de>,
52 | {
53 | struct IdOrDefaultVisitor;
54 | impl serde::de::Visitor<'_> for IdOrDefaultVisitor {
55 | type Value = IdOrDefault;
56 |
57 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
58 | formatter.write_str("a string or 'default'")
59 | }
60 |
61 | fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
62 | where
63 | E: serde::de::Error,
64 | {
65 | let variant = if v.to_lowercase() == "default" {
66 | IdOrDefault::Default
67 | } else {
68 | IdOrDefault::Id(v.to_string())
69 | };
70 |
71 | Ok(variant)
72 | }
73 | }
74 |
75 | deserializer.deserialize_str(IdOrDefaultVisitor)
76 | }
77 | }
78 |
79 | #[cfg(test)]
80 | mod test {
81 | use super::IdOrDefault;
82 |
83 | #[test]
84 | fn id_parses() {
85 | let id = "something";
86 |
87 | let actual: IdOrDefault =
88 | serde_json::from_value(serde_json::Value::String(id.into())).unwrap();
89 | let expected = IdOrDefault::Id(id.to_string());
90 |
91 | assert_eq!(actual, expected);
92 | }
93 |
94 | #[test]
95 | fn default_parses() {
96 | let id = "dEfAuLt";
97 |
98 | let actual: IdOrDefault =
99 | serde_json::from_value(serde_json::Value::String(id.into())).unwrap();
100 | let expected = IdOrDefault::Default;
101 |
102 | assert_eq!(actual, expected);
103 | }
104 | }
105 |
```
--------------------------------------------------------------------------------
/graphql/weather/weather.graphql:
--------------------------------------------------------------------------------
```graphql
1 | extend schema
2 | @link(url: "https://specs.apollo.dev/federation/v2.10", import: ["@tag"])
3 | @link(
4 | url: "https://specs.apollo.dev/connect/v0.1"
5 | import: ["@connect", "@source"]
6 | )
7 | @source(
8 | name: "NWS"
9 | http: {
10 | baseURL: "https://api.weather.gov"
11 | headers: [
12 | { name: "User-Agent", value: "weather-app/1.0" }
13 | { name: "Accept", value: "application/geo+json" }
14 | ]
15 | }
16 | )
17 |
18 | type Query {
19 | """
20 | Get the weather forecast for a coordinate
21 | """
22 | forecast(coordinate: InputCoordinate!): Forecast
23 | @connect(
24 | source: "NWS"
25 | http: { GET: "/points/{$args.coordinate.latitude},{$args.coordinate.longitude}" }
26 | selection: """
27 | coordinate: {
28 | latitude: $args.coordinate.latitude
29 | longitude: $args.coordinate.longitude
30 | }
31 | forecastURL: properties.forecast
32 | """
33 | entity: true
34 | )
35 |
36 | """
37 | Get the weather alerts for a state, using the two-letter abbreviation for the state - for example, CO for Colorado
38 | """
39 | alerts(state: String!): [Alert]
40 | @tag(name: "mcp")
41 | @connect(
42 | source: "NWS"
43 | http: { GET: "/alerts/active/area/{$args.state}" }
44 | selection: """
45 | $.features.properties {
46 | severity
47 | description
48 | instruction
49 | }
50 | """
51 | )
52 | }
53 |
54 | """
55 | A weather forecast
56 | """
57 | type Forecast {
58 | """
59 | The coordinate associated with this forecast
60 | """
61 | coordinate: Coordinate!
62 |
63 | """
64 | The National Weather Service (NWS) URL where the forecast data can be read
65 | """
66 | forecastURL: String!
67 |
68 | """
69 | A detailed weather forecast from the National Weather Service (NWS)
70 | """
71 | detailed: String!
72 | @connect(
73 | http: {
74 | # GET: "{$this.forecastURL->urlSafe}" # TODO: Use this when urlSafe is implemented
75 | GET: "https://api.weather.gov/gridpoints/FFC/51,87/forecast" # TODO: remove this hardcoded value
76 | headers: [
77 | { name: "foo", value: "{$this.forecastURL}" } # required to make composition not throw a satisfiability error
78 | { name: "Accept", value: "application/geo+json" }
79 | { name: "User-Agent", value: "weather-app/1.0" }
80 | ]
81 | }
82 | selection: """
83 | $.properties.periods->first.detailedForecast
84 | """
85 | )
86 | }
87 |
88 | """
89 | A weather alert
90 | """
91 | type Alert @tag(name: "mcp") {
92 | """
93 | The severity of this alert
94 | """
95 | severity: String
96 |
97 | """
98 | A description of the alert
99 | """
100 | description: String
101 |
102 | """
103 | Information about how people should respond to the alert
104 | """
105 | instruction: String
106 | }
107 |
108 | """
109 | A coordinate, consisting of a latitude and longitude
110 | """
111 | input InputCoordinate {
112 | """
113 | The latitude of this coordinate
114 | """
115 | latitude: String!
116 |
117 | """
118 | The longitude of this coordinate
119 | """
120 | longitude: String!
121 | }
122 |
123 |
124 | """
125 | A coordinate, consisting of a latitude and longitude
126 | """
127 | type Coordinate {
128 | """
129 | The latitude of this coordinate
130 | """
131 | latitude: String!
132 |
133 | """
134 | The longitude of this coordinate
135 | """
136 | longitude: String!
137 | }
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/testdata/supergraph.graphql:
--------------------------------------------------------------------------------
```graphql
1 | schema
2 | @link(url: "https://specs.apollo.dev/link/v1.0")
3 | @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
4 | query: Query
5 | }
6 |
7 | directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
8 |
9 | directive @join__field(
10 | graph: join__Graph
11 | requires: join__FieldSet
12 | provides: join__FieldSet
13 | type: String
14 | external: Boolean
15 | override: String
16 | usedOverridden: Boolean
17 | ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
18 |
19 | directive @join__graph(name: String!, url: String!) on ENUM_VALUE
20 |
21 | directive @join__implements(
22 | graph: join__Graph!
23 | interface: String!
24 | ) repeatable on OBJECT | INTERFACE
25 |
26 | directive @join__type(
27 | graph: join__Graph!
28 | key: join__FieldSet
29 | extension: Boolean! = false
30 | resolvable: Boolean! = true
31 | isInterfaceObject: Boolean! = false
32 | ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
33 |
34 | directive @join__unionMember(
35 | graph: join__Graph!
36 | member: String!
37 | ) repeatable on UNION
38 |
39 | directive @link(
40 | url: String
41 | as: String
42 | for: link__Purpose
43 | import: [link__Import]
44 | ) repeatable on SCHEMA
45 |
46 | scalar join__FieldSet
47 |
48 | enum join__Graph {
49 | ACCOUNTS
50 | @join__graph(name: "accounts", url: "https://accounts.demo.starstuff.dev/")
51 | INVENTORY
52 | @join__graph(
53 | name: "inventory"
54 | url: "https://inventory.demo.starstuff.dev/"
55 | )
56 | PRODUCTS
57 | @join__graph(name: "products", url: "https://products.demo.starstuff.dev/")
58 | REVIEWS
59 | @join__graph(name: "reviews", url: "https://reviews.demo.starstuff.dev/")
60 | }
61 |
62 | scalar link__Import
63 |
64 | enum link__Purpose {
65 | SECURITY
66 | EXECUTION
67 | }
68 |
69 | type Product
70 | @join__type(graph: INVENTORY, key: "upc")
71 | @join__type(graph: PRODUCTS, key: "upc")
72 | @join__type(graph: REVIEWS, key: "upc") {
73 | upc: String!
74 | weight: Int
75 | @join__field(graph: INVENTORY, external: true)
76 | @join__field(graph: PRODUCTS)
77 | price: Int
78 | @join__field(graph: INVENTORY, external: true)
79 | @join__field(graph: PRODUCTS)
80 | inStock: Boolean @join__field(graph: INVENTORY)
81 | shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight")
82 | name: String @join__field(graph: PRODUCTS)
83 | reviews: [Review] @join__field(graph: REVIEWS)
84 | }
85 |
86 | type Query
87 | @join__type(graph: ACCOUNTS)
88 | @join__type(graph: INVENTORY)
89 | @join__type(graph: PRODUCTS)
90 | @join__type(graph: REVIEWS) {
91 | me: User @join__field(graph: ACCOUNTS)
92 | topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS)
93 | }
94 |
95 | type Review @join__type(graph: REVIEWS, key: "id") {
96 | id: ID!
97 | body: String
98 | author: User @join__field(graph: REVIEWS, provides: "username")
99 | product: Product @join__field(graph: REVIEWS)
100 | }
101 |
102 | type User
103 | @join__type(graph: ACCOUNTS, key: "id")
104 | @join__type(graph: REVIEWS, key: "id") {
105 | id: ID!
106 | name: String @join__field(graph: ACCOUNTS)
107 | username: String
108 | @join__field(graph: ACCOUNTS)
109 | @join__field(graph: REVIEWS, external: true)
110 | reviews: [Review] @join__field(graph: REVIEWS)
111 | }
112 |
```
--------------------------------------------------------------------------------
/e2e/mcp-server-tester/pq-manifest/apollo.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "format": "apollo-persisted-query-manifest",
3 | "version": 1,
4 | "operations": [
5 | {
6 | "id": "1417c051c5b1ba2fa41975fc02547c9c34c619c8694bf225df74e7b527575d5f",
7 | "name": "ExploreCelestialBodies",
8 | "type": "query",
9 | "body": "query ExploreCelestialBodies($search: String, $limit: Int = 10, $offset: Int = 0) {\n celestialBodies(search: $search, limit: $limit, offset: $offset) {\n pageInfo {\n count\n next\n previous\n __typename\n }\n results {\n id\n name\n diameter\n mass\n gravity\n lengthOfDay\n atmosphere\n type {\n id\n name\n __typename\n }\n image {\n url\n thumbnail\n credit\n __typename\n }\n description\n wikiUrl\n __typename\n }\n __typename\n }\n}"
10 | },
11 | {
12 | "id": "5cc5c30ad71bdf7d57e4fa5a8428c2d49ebc3e16a3d17f21efbd1ad22b4ba70b",
13 | "name": "GetAstronautDetails",
14 | "type": "query",
15 | "body": "query GetAstronautDetails($astronautId: ID!) {\n astronaut(id: $astronautId) {\n id\n name\n status\n inSpace\n age\n dateOfBirth\n dateOfDeath\n firstFlight\n lastFlight\n timeInSpace\n evaTime\n agency {\n id\n name\n abbrev\n country {\n name\n nationalityName\n __typename\n }\n __typename\n }\n nationality {\n name\n nationalityName\n alpha2Code\n __typename\n }\n image {\n url\n thumbnail\n credit\n __typename\n }\n bio\n wiki\n socialMediaLinks {\n url\n socialMedia {\n name\n url\n __typename\n }\n __typename\n }\n __typename\n }\n}"
16 | },
17 | {
18 | "id": "83af5184f29c1eb5ce9b0d6da11285829f2f155d3815affbe66b56fa249f7603",
19 | "name": "GetAstronautsCurrentlyInSpace",
20 | "type": "query",
21 | "body": "query GetAstronautsCurrentlyInSpace {\n astronauts(filters: {inSpace: true, search: \"\"}) {\n results {\n id\n name\n timeInSpace\n lastFlight\n agency {\n name\n abbrev\n country {\n name\n __typename\n }\n __typename\n }\n nationality {\n name\n nationalityName\n __typename\n }\n image {\n thumbnail\n __typename\n }\n __typename\n }\n __typename\n }\n}"
22 | },
23 | {
24 | "id": "824e3c8a1612c32a315450abbd5c7aedc0c402fdf6068583a54461f5b67d55be",
25 | "name": "SearchUpcomingLaunches",
26 | "type": "query",
27 | "body": "query SearchUpcomingLaunches($query: String!) {\n upcomingLaunches(limit: 20, search: $query) {\n pageInfo {\n count\n __typename\n }\n results {\n id\n name\n weatherConcerns\n rocket {\n id\n configuration {\n fullName\n __typename\n }\n __typename\n }\n mission {\n name\n description\n __typename\n }\n webcastLive\n provider {\n name\n __typename\n }\n __typename\n }\n __typename\n }\n}"
28 | }
29 | ]
30 | }
```
--------------------------------------------------------------------------------
/graphql/TheSpaceDevs/persisted_queries/apollo.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "format": "apollo-persisted-query-manifest",
3 | "version": 1,
4 | "operations": [
5 | {
6 | "id": "1417c051c5b1ba2fa41975fc02547c9c34c619c8694bf225df74e7b527575d5f",
7 | "name": "ExploreCelestialBodies",
8 | "type": "query",
9 | "body": "query ExploreCelestialBodies($search: String, $limit: Int = 10, $offset: Int = 0) {\n celestialBodies(search: $search, limit: $limit, offset: $offset) {\n pageInfo {\n count\n next\n previous\n __typename\n }\n results {\n id\n name\n diameter\n mass\n gravity\n lengthOfDay\n atmosphere\n type {\n id\n name\n __typename\n }\n image {\n url\n thumbnail\n credit\n __typename\n }\n description\n wikiUrl\n __typename\n }\n __typename\n }\n}"
10 | },
11 | {
12 | "id": "5cc5c30ad71bdf7d57e4fa5a8428c2d49ebc3e16a3d17f21efbd1ad22b4ba70b",
13 | "name": "GetAstronautDetails",
14 | "type": "query",
15 | "body": "query GetAstronautDetails($astronautId: ID!) {\n astronaut(id: $astronautId) {\n id\n name\n status\n inSpace\n age\n dateOfBirth\n dateOfDeath\n firstFlight\n lastFlight\n timeInSpace\n evaTime\n agency {\n id\n name\n abbrev\n country {\n name\n nationalityName\n __typename\n }\n __typename\n }\n nationality {\n name\n nationalityName\n alpha2Code\n __typename\n }\n image {\n url\n thumbnail\n credit\n __typename\n }\n bio\n wiki\n socialMediaLinks {\n url\n socialMedia {\n name\n url\n __typename\n }\n __typename\n }\n __typename\n }\n}"
16 | },
17 | {
18 | "id": "83af5184f29c1eb5ce9b0d6da11285829f2f155d3815affbe66b56fa249f7603",
19 | "name": "GetAstronautsCurrentlyInSpace",
20 | "type": "query",
21 | "body": "query GetAstronautsCurrentlyInSpace {\n astronauts(filters: {inSpace: true, search: \"\"}) {\n results {\n id\n name\n timeInSpace\n lastFlight\n agency {\n name\n abbrev\n country {\n name\n __typename\n }\n __typename\n }\n nationality {\n name\n nationalityName\n __typename\n }\n image {\n thumbnail\n __typename\n }\n __typename\n }\n __typename\n }\n}"
22 | },
23 | {
24 | "id": "824e3c8a1612c32a315450abbd5c7aedc0c402fdf6068583a54461f5b67d55be",
25 | "name": "SearchUpcomingLaunches",
26 | "type": "query",
27 | "body": "query SearchUpcomingLaunches($query: String!) {\n upcomingLaunches(limit: 20, search: $query) {\n pageInfo {\n count\n __typename\n }\n results {\n id\n name\n weatherConcerns\n rocket {\n id\n configuration {\n fullName\n __typename\n }\n __typename\n }\n mission {\n name\n description\n __typename\n }\n webcastLive\n provider {\n name\n __typename\n }\n __typename\n }\n __typename\n }\n}"
28 | }
29 | ]
30 | }
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-server/src/errors.rs:
--------------------------------------------------------------------------------
```rust
1 | use crate::introspection::tools::search::IndexingError;
2 | use apollo_compiler::{Schema, ast::Document, validation::WithErrors};
3 | use apollo_federation::error::FederationError;
4 | use apollo_mcp_registry::platform_api::operation_collections::error::CollectionError;
5 | use reqwest::header::{InvalidHeaderName, InvalidHeaderValue};
6 | use rmcp::serde_json;
7 | use tokio::task::JoinError;
8 | use url::ParseError;
9 |
10 | /// An error in operation parsing
11 | #[derive(Debug, thiserror::Error)]
12 | pub enum OperationError {
13 | #[error("Could not parse GraphQL document: {0}")]
14 | GraphQLDocument(Box<WithErrors<Document>>),
15 |
16 | #[error("Internal error: {0}")]
17 | Internal(String),
18 |
19 | #[error("{0}Operation is missing its required name: {1}", .source_path.as_ref().map(|s| format!("{s}: ")).unwrap_or_default(), operation)]
20 | MissingName {
21 | source_path: Option<String>,
22 | operation: String,
23 | },
24 |
25 | #[error("{0}No operations defined", .source_path.as_ref().map(|s| format!("{s}: ")).unwrap_or_default())]
26 | NoOperations { source_path: Option<String> },
27 |
28 | #[error("Invalid JSON: {0}")]
29 | Json(#[from] serde_json::Error),
30 |
31 | #[error("{0}Too many operations. Expected 1 but got {1}", .source_path.as_ref().map(|s| format!("{s}: ")).unwrap_or_default(), count)]
32 | TooManyOperations {
33 | source_path: Option<String>,
34 | count: usize,
35 | },
36 |
37 | #[error(transparent)]
38 | File(#[from] std::io::Error),
39 |
40 | #[error("Error loading collection: {0}")]
41 | Collection(CollectionError),
42 | }
43 |
44 | /// An error in server initialization
45 | #[derive(Debug, thiserror::Error)]
46 | pub enum ServerError {
47 | #[error("Could not parse GraphQL document: {0}")]
48 | GraphQLDocument(Box<WithErrors<Document>>),
49 |
50 | #[error("Could not parse GraphQL schema: {0}")]
51 | GraphQLSchema(Box<WithErrors<Schema>>),
52 |
53 | #[error("Could not parse GraphQL schema: {0}")]
54 | GraphQLDocumentSchema(Box<WithErrors<Document>>),
55 |
56 | #[error("Federation error in GraphQL schema: {0}")]
57 | Federation(Box<FederationError>),
58 |
59 | #[error("Invalid JSON: {0}")]
60 | Json(#[from] serde_json::Error),
61 |
62 | #[error("Failed to create operation: {0}")]
63 | Operation(#[from] OperationError),
64 |
65 | #[error("Could not open file: {0}")]
66 | ReadFile(#[from] std::io::Error),
67 |
68 | #[error("invalid header value: {0}")]
69 | HeaderValue(#[from] InvalidHeaderValue),
70 |
71 | #[error("invalid header name: {0}")]
72 | HeaderName(#[from] InvalidHeaderName),
73 |
74 | #[error("invalid header: {0}")]
75 | Header(String),
76 |
77 | #[error("invalid custom_scalar_config: {0}")]
78 | CustomScalarConfig(serde_json::Error),
79 |
80 | #[error("invalid json schema: {0}")]
81 | CustomScalarJsonSchema(String),
82 |
83 | #[error("Missing environment variable: {0}")]
84 | EnvironmentVariable(String),
85 |
86 | #[error("You must define operations or enable introspection")]
87 | NoOperations,
88 |
89 | #[error("No valid schema was supplied")]
90 | NoSchema,
91 |
92 | #[error("Failed to start server")]
93 | StartupError(#[from] JoinError),
94 |
95 | #[error("Failed to initialize MCP server")]
96 | McpInitializeError(#[from] Box<rmcp::service::ServerInitializeError>),
97 |
98 | #[error(transparent)]
99 | UrlParseError(ParseError),
100 |
101 | #[error("Failed to index schema: {0}")]
102 | Indexing(#[from] IndexingError),
103 |
104 | #[error("CORS configuration error: {0}")]
105 | Cors(String),
106 | }
107 |
108 | /// An MCP tool error
109 | pub type McpError = rmcp::model::ErrorData;
110 |
```
--------------------------------------------------------------------------------
/crates/apollo-mcp-registry/src/uplink/persisted_queries/manifest.rs:
--------------------------------------------------------------------------------
```rust
1 | use std::collections::HashMap;
2 | use std::ops::Deref;
3 | use std::ops::DerefMut;
4 |
5 | use serde::Deserialize;
6 | use serde::Serialize;
7 | use tower::BoxError;
8 |
9 | /// The full identifier for an operation in a PQ list consists of an operation
10 | /// ID and an optional client name.
11 | #[derive(Debug, Clone, Eq, Hash, PartialEq)]
12 | pub struct FullPersistedQueryOperationId {
13 | /// The operation ID (usually a hash).
14 | pub operation_id: String,
15 | /// The client name associated with the operation; if None, can be any client.
16 | pub client_name: Option<String>,
17 | }
18 |
19 | /// A single operation containing an ID and a body.
20 | #[derive(Debug, Clone, Deserialize, Serialize)]
21 | #[serde(rename_all = "camelCase")]
22 | pub struct ManifestOperation {
23 | /// The operation ID (usually a hash).
24 | pub id: String,
25 | /// The operation body.
26 | pub body: String,
27 | /// The client name associated with the operation. If None, can be any client.
28 | pub client_name: Option<String>,
29 | }
30 |
31 | /// The format of each persisted query chunk returned from uplink.
32 | #[derive(Debug, Clone, Deserialize, Serialize)]
33 | pub struct SignedUrlChunk {
34 | pub format: String,
35 | pub version: u64,
36 | pub operations: Vec<ManifestOperation>,
37 | }
38 |
39 | impl SignedUrlChunk {
40 | pub fn validate(self) -> Result<Self, BoxError> {
41 | if self.format != "apollo-persisted-query-manifest" {
42 | return Err("chunk format is not 'apollo-persisted-query-manifest'".into());
43 | }
44 |
45 | if self.version != 1 {
46 | return Err("persisted query manifest chunk version is not 1".into());
47 | }
48 |
49 | Ok(self)
50 | }
51 |
52 | pub fn parse_and_validate(raw_chunk: &str) -> Result<Self, BoxError> {
53 | let parsed_chunk =
54 | serde_json::from_str::<SignedUrlChunk>(raw_chunk).map_err(|e| -> BoxError {
55 | format!("Could not parse persisted query manifest chunk: {e}").into()
56 | })?;
57 |
58 | parsed_chunk.validate()
59 | }
60 | }
61 |
62 | /// An in memory cache of persisted queries.
63 | #[derive(Debug, Clone, Default)]
64 | pub struct PersistedQueryManifest {
65 | inner: HashMap<FullPersistedQueryOperationId, String>,
66 | }
67 |
68 | impl PersistedQueryManifest {
69 | /// Add a chunk to the manifest.
70 | pub fn add_chunk(&mut self, chunk: &SignedUrlChunk) {
71 | for operation in &chunk.operations {
72 | self.inner.insert(
73 | FullPersistedQueryOperationId {
74 | operation_id: operation.id.clone(),
75 | client_name: operation.client_name.clone(),
76 | },
77 | operation.body.clone(),
78 | );
79 | }
80 | }
81 | }
82 |
83 | impl From<Vec<ManifestOperation>> for PersistedQueryManifest {
84 | fn from(operations: Vec<ManifestOperation>) -> Self {
85 | let mut manifest = PersistedQueryManifest::default();
86 | for operation in operations {
87 | manifest.insert(
88 | FullPersistedQueryOperationId {
89 | operation_id: operation.id,
90 | client_name: operation.client_name,
91 | },
92 | operation.body,
93 | );
94 | }
95 | manifest
96 | }
97 | }
98 |
99 | impl Deref for PersistedQueryManifest {
100 | type Target = HashMap<FullPersistedQueryOperationId, String>;
101 |
102 | fn deref(&self) -> &Self::Target {
103 | &self.inner
104 | }
105 | }
106 |
107 | impl DerefMut for PersistedQueryManifest {
108 | fn deref_mut(&mut self) -> &mut Self::Target {
109 | &mut self.inner
110 | }
111 | }
112 |
```