MCP Apps Development

Key concepts when developing MCP Apps


This document explains the core concepts of developing MCP Apps using React, Apollo Client, and Apollo MCP Server.

We recommend that you use the Apollo AI Apps Template to get started developing your app. The template includes much of the code referenced in this document.

Apollo Client initialization

If you have experience developing with Apollo Client, you already know most of what you need to know to develop with MCP Apps, which uses the @apollo/client-ai-apps package to manage GraphQL operations. However, this guide doesn't provide a comprehensive explanation of Apollo Client. To learn about it, go to the Apollo Client documentation.

You initialize ApolloClient by using the ApolloClient class provided by @apollo/client-ai-apps. The ApolloClient constructor needs at least an instance of ApolloCache (typically InMemoryCache) and the manifest file.

caution
Make sure you import ApolloClient from the @apollo/client-ai-apps package, not the core @apollo/client repository. The ApolloClient class from @apollo/client-ai-apps is for integrating with the MCP Apps environment.
TypeScript
src/main.tsx
1import { InMemoryCache } from "@apollo/client";
2import { ApolloClient, ApplicationManifest } from "@apollo/client-ai-apps";
3// Note the manifest is written to the root of the app
4import manifest from "../.application-manifest.json";
5
6// You may also provide other options that the core ApolloClient accepts
7const client = new ApolloClient({
8  cache: new InMemoryCache(),
9  manifest: manifest as ApplicationManifest,
10});
note
Unlike the ApolloClient class from the core @apollo/client package, the ApolloClient class from @apollo/client-ai-apps doesn't require a configured link. By default, the class uses ToolCallLink, which executes GraphQL queries through MCP tools.

You can provide a custom link chain to grant additional capabilities to ApolloClient; however, your terminating link needs to be a ToolCallLink.

Provide your client to ApolloProvider

After you've created your client instance, pass it to ApolloProvider. As with ApolloClient, you need to use the ApolloProvider component exported from the @apollo/client-ai-apps package.

TypeScript
src/main.tsx
1import { ApolloProvider } from "@apollo/client-ai-apps/react";
2
3const client = new ApolloClient({
4  // ...
5});
6
7createRoot(document.getElementById("root")!).render(
8  <ApolloProvider client={client}>
9    <App />
10  </ApolloProvider>,
11);

The ApolloProvider implementation from @apollo/client-ai-apps/react initializes your application with Apollo MCP Server and provides the client instance in React context for use with Apollo Client's data-fetching hooks.

Show a loading fallback during initialization

The ApolloProvider component uses React's Suspense functionality during initialization, which avoids rendering application code that relies on client initialization. The client is initialized when it receives the ui/notifications/tool-result notification from the host.

If you want to display a loading fallback while the app initializes, wrap ApolloProvider with React's Suspense component.

TypeScript
1import { Suspense } from "react";
2
3createRoot(document.getElementById("root")!).render(
4  <Suspense fallback={<LoadingFallback />}>
5    <ApolloProvider client={client}>
6      <App />
7    </ApolloProvider>
8  </Suspense>,
9);
note
Using a Suspense component to display a loading fallback is optional. If you don't provide a Suspense component, the screen remains blank until the host provides the tool result.

Register an MCP tool

To register MCP tools, combine your GraphQL queries with the @tool directive.

For example, if you want to define a query that gets the highest-rated products in a marketplace application, define a query called TopProductsQuery in your React component, then use the useQuery hook from @apollo/client/react to read the query data. To register the query as its own tool, use the @tool directive.

TypeScript
1import { gql, TypedDocumentNode } from "@apollo/client";
2import { useQuery } from "@apollo/client/react";
3
4// Note: These types should be generated using a tool like GraphQL Codegen.
5// Avoid writing them by hand.
6const TOP_PRODUCTS_QUERY: TypedDocumentNode<
7  TopProductsQuery,
8  TopProductsQueryVariables
9> = gql`
10  query TopProductsQuery
11  @tool(
12    name: "TopProducts"
13    description: "Shows a list of the highest rated products."
14  ) {
15    topProducts {
16      id
17      title
18      rating
19      price
20      thumbnail
21    }
22  }
23`;
24
25function App() {
26  const { data, loading, error, dataState } = useQuery(TOP_PRODUCTS_QUERY);
27
28  if (loading) {
29    return <div>Loading...</div>;
30  }
31
32  if (error) {
33    return <div>Error! {error.message}</div>;
34  }
35
36  return (
37    <div>
38      {data.topProducts.map((product) => (
39        <Product key={product.id} product={product} />
40      ))}
41    </div>
42  );
43}

Now that the query is registered as a tool, you can run the app in ChatGPT or an MCP Apps-compatible host and prompt the LLM to show you the top products. They render using data fetched by MCP Server.

Platform-specific modules

To create platform-specific modules, append the file extension that corresponds with the host you want to target.

  • *.openai.ts(x) - Code specific to a ChatGPT app

  • *.mcp.ts(x) - Code specific to an MCP app

Those extensions allow you to write code only available to the targeted host.

For example, the @apollo/client-ai-apps/openai entry point provides a useWidgetState hook that is only available to ChatGPT apps. To use that hook, create a component file with the .openai.tsx extension.

TypeScript
MyComponent.openai.tsx
1import { useWidgetState } from "@apollo/client-ai-apps/openai";
2
3export function MyComponent() {
4  const [widgetState, setWidgetState] = useWidgetState();
5
6  return (
7    <div>
8      <div>{widgetState.foo.toString()}</div>
9      <button onClick={() => setWidgetState({ foo: true })}>Set foo</button>
10    </div>
11  );
12}
tip
For additional type safety when developing a platform-specific module, please reference the TypeScript configuration section to update your configuration.

How to import platform-specific modules

To import platform-specific modules, use the base name. For example, if you create a platform-specific Home component for OpenAI (Home.openai.tsx) and MCP (Home.mcp.tsx), you import it like this:

TypeScript
1import { Home } from "./Home";
caution
Make sure each configured environment has a module it can import. If one doesn't exist, the application fails to build. For example, if you create a Home.openai.tsx file, you also need to create either a Home.mcp.tsx or Home.tsx file.

TypeScript configuration

@apollo/client-ai-apps provides extendable TypeScript configurations that make sure you only use code available to your application from platform-specific modules.

  • @apollo/client-ai-apps/tsconfig/core — for shared modules that run in both environments. This config ensures that included files only use shared utilities available to all environments. Use this when type-checking shared files for all environments..

  • @apollo/client-ai-apps/tsconfig/mcp — For developing MCP-specific modules. This configuration allows you to access utilities exported by @apollo/client-ai-apps/mcp. Use this when type-checking your .mcp.* files.

  • @apollo/client-ai-apps/tsconfig/openai — For developing ChatGPT-specific modules. This configuration allows you to use utilities exported by @apollo/client-ai-apps/openai. Use this when type-checking your .openai.* files.

Use TypeScript project references to apply a TypeScript configuration to a subset of files. We recommend these configuration files:

Text
1.
2├── tsconfig.app.json
3├── tsconfig.base.json
4├── tsconfig.mcp.json
5├── tsconfig.openai.json
6└── tsconfig.json

tsconfig.base.json

Shared configuration for all TypeScript files. This is be where you should configure most of your compilerOptions.

jsonc
tsconfig.base.json
1{
2  "compilerOptions": {
3    "strict": true,
4    // ... all other preferred TypeScript settings
5  },
6}

tsconfig.app.json

Configuration that targets all shared modules. Extend @apollo/client-ai-apps/tsconfig/core and exclude platform-specific modules (.mcp.* and .openai.* files).

jsonc
tsconfig.app.json
1{
2  "extends": ["@apollo/client-ai-apps/tsconfig/core", "./tsconfig.base.json"],
3  "include": ["src"],
4  "exclude": ["src/**/*.mcp.*", "src/**/*.openai.*"],
5}

tsconfig.mcp.json

Configuration that targets MCP app specific modules. Extend @apollo/client-ai-apps/tsconfig/mcp and include .mcp.* files.

jsonc
tsconfig.mcp.json
1{
2  "extends": ["@apollo/client-ai-apps/tsconfig/mcp", "./tsconfig.base.json"],
3  "include": ["src/**/*.mcp.ts", "src/**/*.mcp.tsx"],
4}
note
If your app doesn't have any MCP Apps-specific files, don't include this file because TypeScript might be unable to match files in the include setting.

tsconfig.openai.json

Configuration that targets ChatGPT App-specific modules. Extend @apollo/client-ai-apps/tsconfig/openai and include .openai.* files. Include the openai/globals types in the types configuration, which provides types for window.openai.

jsonc
tsconfig.openai.json
1{
2  "extends": ["@apollo/client-ai-apps/tsconfig/openai", "./tsconfig.base.json"],
3  "compilerOptions": {
4    "types": ["@apollo/client-ai-apps/openai/globals"],
5  },
6  "include": ["src/**/*.openai.ts", "src/**/*.openai.tsx"],
7}
note
If your app doesn't have any ChatGPT App-specific files, don't include this file because TypeScript might complain that it is unable to match files in the include setting.

tsconfig.json

The base configuration that should include all project references.

jsonc
tsconfig.openai.json
1{
2  "files": [],
3  "references": [
4    { "path": "./tsconfig.app.json" },
5    // Omit if you don't create this file
6    { "path": "./tsconfig.mcp.json" },
7    // Omit if you don't create this file
8    { "path": "./tsconfig.openai.json" },
9  ],
10}

Vite configuration

The @apollo/client-ai-apps package includes a Vite plugin that configures your application for developing ChatGPT and MCP apps.

To use it, import apolloClientAiApps from @apollo/client-ai-apps/vite and provide it to the plugins option in your Vite configuration. Set the targets option to define what environments you are building for.

  • mcp - Creates a build artifact for MCP apps.

  • openai - Creates a build artifact for ChatGPT apps.

TypeScript
vite.config.ts
1import { defineConfig } from "vite";
2import { apolloClientAiApps } from "@apollo/client-ai-apps/vite";
3
4export default defineConfig({
5  plugins: [
6    apolloClientAiApps({
7      targets: ["mcp", "openai"],
8    }),
9    // ... other plugins
10  ],
11  // ... other config
12});

Development mode

You can only run one environment in development mode at a time. Set the devTarget option to define what environment you are developing for.

TypeScript
1apolloClientAiApps({
2  devTarget: "openai",
3  // ...
4});
note
If the configured targets option only includes one value, you can omit the devTarget option because the plugin infers the devTarget as the value in targets.

Use environment variables to set devTarget

If you are developing both ChatGPT and MCP apps, switching the devTarget each time you want to develop for a different environment is cumbersome. We recommend that you use environment variables to configure those kinds of dynamic values so that you can easily change the environment without having to edit the build configuration.

TypeScript
1apolloClientAiApps({
2  devTarget: process.env.DEV_TARGET,
3  // ...
4});

However, this results in a TypeScript error. devTarget only accepts the values mcp or openai, but process.env.DEV_TARGET is a string type. To fix this issue, use the devTarget helper exported from @apollo/client-ai-apps/vite which validates the input and returns the correctly typed value.

TypeScript
1import { apolloClientAiApps, devTarget } from "@apollo/client-ai-apps/vite";
2
3apolloClientAiApps({
4  devTarget: devTarget(process.env.DEV_TARGET),
5  // ...
6});
tip
We recommend that you add separate development scripts in package.json that target each environment so you don't need to remember to set the environment variable.
JSON
package.json
1{
2  "scripts": {
3    "dev:mcp": "DEV_TARGET=mcp vite",
4    "dev:openai": "DEV_TARGET=openai vite"
5  }
6}
Feedback

Edit on GitHub

Ask Community