---
title: MCP Apps Development
subtitle: 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](https://github.com/apollographql/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](../react).

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](./mcp-apps-reference#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.

</Caution>

```ts title=src/main.tsx
import { InMemoryCache } from "@apollo/client";
import { ApolloClient } from "@apollo/client-ai-apps";
// Note the manifest is written to the root of the app
import manifest from "../.application-manifest.json";

// You may also provide other options that the core ApolloClient accepts
const client = new ApolloClient({
  cache: new InMemoryCache(),
  manifest,
});
```

<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. <br/><br/>

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

</Note>

### 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.

```tsx title=src/main.tsx
import { ApolloProvider } from "@apollo/client-ai-apps/react";

const client = new ApolloClient({
  // ...
});

createRoot(document.getElementById("root")!).render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
);
```

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](https://react.dev/reference/react/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](https://react.dev/reference/react/Suspense).

```tsx
import { Suspense } from "react";

createRoot(document.getElementById("root")!).render(
  <Suspense fallback={<LoadingFallback />}>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </Suspense>,
);
```

<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.

</Note>

## Register an MCP tool

To register MCP tools, combine your GraphQL queries with the `@tool` directive. By default, the operation name and description are used to define the tool name and description. `@tool` also accepts `name` and `description` arguments, which override the operation name and description used to register the tool.

<Note>

To use operation descriptions, you must use `graphql` version `16.2.0` or greater.

</Note>

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.

```tsx
import { gql, TypedDocumentNode } from "@apollo/client";
import { useQuery } from "@apollo/client/react";

// Note: These types should be generated using a tool like GraphQL Codegen.
// Avoid writing them by hand.
const TOP_PRODUCTS_QUERY: TypedDocumentNode<
  TopProductsQuery,
  TopProductsQueryVariables
> = gql`
  "Shows a list of the highest-rated products."
  query TopProductsQuery @tool {
    topProducts {
      id
      title
      rating
      price
      thumbnail
    }
  }
`;

function App() {
  const { data, loading, error, dataState } = useQuery(TOP_PRODUCTS_QUERY);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error! {error.message}</div>;
  }

  return (
    <div>
      {data.topProducts.map((product) => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
}
```

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.

## Access tool information

To access the tool name, as well as input passed from the LLM to the tool, use the `useToolInfo` hook.

```tsx
import { useToolInfo } from "@apollo/client-ai-apps/react";

function App() {
  const { toolName, toolInput } = useToolInfo();

  return (
    <div>
      <p>Tool: {toolName}</p>
      <p>Input: {JSON.stringify(toolInput)}</p>
    </div>
  );
}
```

`toolName` and `toolInput` are typed according to the tools defined in your app. To learn how to include the generated types in your app, go to the [typescript configuration](#typescript-configuration) section.

<Note>

To provide specific `toolInput` types, configure a `schema` with the `apolloClientAiApps` Vite plugin. For more information, go to the [Vite configuration](#vite-configuration) section.

</Note>

## Populate variables from tool input

When your query includes variables, the LLM decides what values to provide to those variables. That can be a problem because the variable values you provide to `useQuery` might not match the LLM provided variables.

You might consider using `useToolInfo` to populate the initial values for those variables, but that approach quickly reveals additional complexity that `useToolInfo` cannot handle on its own:

- The tool called by the agent might not be the same tool
- The input might differ from the original tool input when a user navigates away and back
- Some variables might need to synchronize with an external source, such as `props`
- Some variables might change as a result of user interaction

The `createHydrationUtils` utility bridges the gap. Call `createHydrationUtils` with the query that provides the tool definition. Doing that returns a `useHydratedVariables` hook used to manage variables for the query. The returned `variables` are provided to the `useQuery` hook in the `variables` option.

```tsx
import { createHydrationUtils } from "@apollo/client-ai-apps/react";

const TOP_PRODUCTS: TypedDocumentNode<
  TopProductsQuery,
  TopProductsQueryVariables
> = gql`
  "Get list of top products"
  query TopProducts($limit: Int) @tool {
    topProducts(limit: $limit) {
      id
      title
      price
    }
  }
`;

const { useHydratedVariables } = createHydrationUtils(TOP_PRODUCTS);

function App() {
  // Provide initial variable values to the `useHydratedVariables` hook.
  // These are used when the tool call doesn't match this query
  const [variables] = useHydratedVariables({ limit: 10 });

  // Use the `variables` returned from `useHydratedVariables` for the query
  const { data } = useQuery(TOP_PRODUCTS, { variables });

  // ...
}
```

`useHydratedVariables` compares the configured query to the tool called by the agent to determine if they match. When they match, `variables` is strictly set to the value of the tool input provided by the LLM. When they don't match, the `variables` argument passed to `useHydratedVariables` is used instead.

<Note>

`useHydratedVariables` only returns `variables` defined by the query. Extra inputs, such as those added by `extraInputs`, are stripped away in the returned object.

</Note>

<Caution>

Always read values from the `variables` object returned by `useHydratedVariables` to ensure your UI accurately reflects the current state.

</Caution>

### Optional variables

GraphQL queries can include optional variables in the query definition. When the tool executes a query that includes an optional variable, the LLM might omit any optional variables. When that happens, the returned `variables` object includes only the values from the tool input. The variable values provided to `useHydratedVariables` are ignored, ensuring that `useQuery` matches the exact tool call.

```tsx
const TOP_PRODUCTS: TypedDocumentNode<
  TopProductsQuery,
  TopProductsQueryVariables
> = gql`
  "Get list of top products"
  query TopProducts($limit: Int, $order: Order, $sortBy: SortBy) @tool {
    topProducts(limit: $limit, order: $order, sortBy: $sortBy) {
      id
      title
      price
    }
  }
`;

const { useHydratedVariables } = createHydrationUtils(TOP_PRODUCTS);

function App() {
  // if tool input is:
  // { limit: 5 }
  const [variables] = useHydratedVariables({
    limit: 10,
    order: "asc",
    sortBy: "title",
  });
  // => { limit: 5 }
}
```

### Update variables during user interaction

Similar to the `useState` hook in React, `useHydratedVariables` returns both the current `variables` value and a `setVariables` function, used to update the value of a variable. Call `setVariables` with a partial variables object that includes the variables you want to update. `setVariables` shallow-merges the current variables object with the updated variables object so you don't need to provide the full variables object each time you want to make an update.

```tsx
const TOP_PRODUCTS: TypedDocumentNode<
  TopProductsQuery,
  TopProductsQueryVariables
> = gql`
  "Get list of top products"
  query TopProducts($limit: Int) @tool {
    topProducts(limit: $limit) {
      id
      title
      price
    }
  }
`;

const { useHydratedVariables } = createHydrationUtils(TOP_PRODUCTS);

function App() {
  const [variables, setVariables] = useHydratedVariables({ limit: 10 });

  const { data } = useQuery(TOP_PRODUCTS, { variables });

  function handleShowMore() {
    setVariables({ limit: variables.limit + 5 });

    // Use a callback to get the previous variables value
    setVariables((prev) => ({ limit: prev.limit + 5 }));
  }

  return (
    <>
      <button onClick={handleShowMore}>Show 5 more results</button>

      {/* ... */}
    </>
  );
}
```

### Sync variables with props

You might have variable values that are provided from an external source like props. You typically want to keep those values up-to-date without having to sync the values manually.

To mark a variable as reactive, use the `reactive` function.

```tsx
import { createHydrationUtils, reactive } from "@apollo/client-ai-apps/react";

const GET_PRODUCTS_BY_CATEGORY = gql`
  "Get an ordered list of products for a category"
  query GetProductsByCategory($category: Category!, $order: Order, $limit: Int)
  @tool {
    productsByCategory(category: $category, order: $order, limit: $limit) {
      id
      title
      price
    }
  }
`;

const { useHydratedVariables } = createHydrationUtils(GET_PRODUCTS_BY_CATEGORY);

interface AppProps {
  id: string;
}

function App({ id }: AppProps) {
  // As `id` changes, `variables.id` is kept in sync with the value
  const [variables, setVariables] = useHydratedVariables({
    id: reactive(id),
    order: "asc",
    limit: 10,
  });

  const { data } = useQuery(GET_PRODUCTS_BY_CATEGORY, { variables });
  // ...
}
```

Marking a variable `reactive` prevents you from using that variable in the `setVariables` function, which avoids accidental bugs that might desynchronize the value with the input. If a reactive variable is provided to `setVariables`, a warning is logged and the update is ignored.

<Note>

When the value of a reactive variable differs from the tool input value, the tool input value is used. It remains the value of the tool input value until the external value provided to `reactive` changes. From then on, the new value is used.

</Note>

## Access host context

Use the `useHostContext` hook to access context provided by the MCP host. `useHostContext` keeps the host context up-to-date for you as it changes.

```tsx title=MyComponent.mcp.tsx
import { useHostContext } from "@apollo/client-ai-apps";

export function MyComponent() {
  const hostContext = useHostContext();

  return <div>{JSON.stringify(hostContext)}</div>;
}
```

## 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.

```tsx title=MyComponent.openai.tsx
import { useWidgetState } from "@apollo/client-ai-apps/openai";

export function MyComponent() {
  const [widgetState, setWidgetState] = useWidgetState();

  return (
    <div>
      <div>{widgetState.foo.toString()}</div>
      <button onClick={() => setWidgetState({ foo: true })}>Set foo</button>
    </div>
  );
}
```

<Tip>

For additional type safety when developing a platform-specific module, please reference the [TypeScript configuration](#typescript-configuration) section to update your configuration.

</Tip>

### 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:

```tsx
import { 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.

</Caution>

## `Platform` utility

In some cases, you might want to change a value in your application depending on the running platform. The `Platform` utility provides a simple way to detect the current platform and provide platform-specific values at runtime.

```tsx
import { Platform } from "@apollo/client-ai-apps";

function App() {
  // Use `Platform.target` to get a string representing the current platform
  const currentPlatform = Platform.target; // => "openai" | "mcp"

  // The `Platform.select` function selects the value for you using the current platform
  const label = Platform.select({
    mcp: "Running in an MCP host",
    openai: "Running in ChatGPT",
  });

  // `Platform.select` also accepts a function that is called immediately.
  // Useful when you want to avoid expensive computation for a value not used in
  // the current platform
  const value = Platform.select({
    mcp: () => factorial(10),
    openai: () => factorial(20),
  });

  return <div>{label}</div>;
}
```

<Note>

Any value can be returned by `select`. You aren't limited to strings.

</Note>

## 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](#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.

### Recommended TypeScript configuration

Use TypeScript [project references](https://www.typescriptlang.org/docs/handbook/project-references.html) to apply a TypeScript configuration to a subset of files. We recommend these configuration files:

```
.
├── tsconfig.app.json
├── tsconfig.base.json
├── tsconfig.mcp.json
├── tsconfig.openai.json
└── tsconfig.json
```

**`tsconfig.base.json`**

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

```jsonc title=tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    // ... all other preferred TypeScript settings
  },
}
```

**`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). Make sure you also include the generated types from the `apolloClientAiApps` Vite plugin.

```jsonc title=tsconfig.app.json
{
  "extends": ["@apollo/client-ai-apps/tsconfig/core", "./tsconfig.base.json"],
  "include": ["src", ".apollo-client-ai-apps/types"],
  "exclude": ["src/**/*.mcp.*", "src/**/*.openai.*"],
}
```

**`tsconfig.mcp.json`**

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

```jsonc title=tsconfig.mcp.json
{
  "extends": ["@apollo/client-ai-apps/tsconfig/mcp", "./tsconfig.base.json"],
  "include": [
    ".apollo-client-ai-apps/types",
    "src/**/*.mcp.ts",
    "src/**/*.mcp.tsx",
  ],
}
```

<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.

</Note>

**`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 title=tsconfig.openai.json
{
  "extends": ["@apollo/client-ai-apps/tsconfig/openai", "./tsconfig.base.json"],
  "compilerOptions": {
    "types": ["@apollo/client-ai-apps/openai/globals"],
  },
  "include": [
    ".apollo-client-ai-apps/types",
    "src/**/*.openai.ts",
    "src/**/*.openai.tsx",
  ],
}
```

Note that, if [`compilerOptions.types`](https://www.typescriptlang.org/tsconfig#types) is specified, only those packages are included in the global scope. That is the recommendation for TypeScript version 5.9 and the default in version [6.0](https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/#types-now-defaults-to-[])

<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.

</Note>

**`tsconfig.json`**

The base configuration that should include all project references.

```jsonc title=tsconfig.openai.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    // Omit if you don't create this file
    { "path": "./tsconfig.mcp.json" },
    // Omit if you don't create this file
    { "path": "./tsconfig.openai.json" },
  ],
}
```

## Vite configuration

The `@apollo/client-ai-apps` package includes a [Vite](https://vite.dev/) 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.

```ts title=vite.config.ts
import { defineConfig } from "vite";
import { apolloClientAiApps } from "@apollo/client-ai-apps/vite";

export default defineConfig({
  plugins: [
    apolloClientAiApps({
      targets: ["mcp", "openai"],
      appsOutDir: "../apps", // path where build artifacts are written
    }),
    // ... other plugins
  ],
  // ... other config
});
```

The `appsOutDir` option controls where build artifacts (including the manifest file and built resources) are written. That directory must be named `apps`, and it should point to the directory that Apollo MCP Server reads from.

<Tip>

The Vite plugin creates a `.apollo-client-ai-apps/` directory with generated files, such as TypeScript types for your application. This directory is fully generated by the Vite plugin and can be safely added to your `.gitignore`.

</Tip>

### 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.

```ts
apolloClientAiApps({
  devTarget: "openai",
  // ...
});
```

<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`.

</Note>

#### 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.

```ts
apolloClientAiApps({
  devTarget: process.env.DEV_TARGET,
  // ...
});
```

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.

```ts
import { apolloClientAiApps, devTarget } from "@apollo/client-ai-apps/vite";

apolloClientAiApps({
  devTarget: devTarget(process.env.DEV_TARGET),
  // ...
});
```

<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 title=package.json
{
  "scripts": {
    "dev:mcp": "DEV_TARGET=mcp vite",
    "dev:openai": "DEV_TARGET=openai vite"
  }
}
```

</Tip>

### Schema types

`apolloClientAiApps` generates TypeScript types for tools defined in your application by the `@tool` directive, including tool names and inputs. For input types to resolve correctly, ensure the Vite plugin knows about input types defined by your GraphQL schema. Configure your schema using the `schema` option.

```ts
apolloClientAiApps({
  schema: "./path/to/schema.graphql",
  // ...
});
```

If you don't configure a `schema`, the resolved tool input types are defined as `Record<string, unknown>`.

<Note>

The `apolloClientAiApps` Vite plugin uses [GraphQL Codegen](https://the-guild.dev/graphql/codegen) internally. The `schema` option accepts any value that [GraphQL Codegen accepts](https://the-guild.dev/graphql/codegen/docs/config-reference/schema-field#available-formats).

</Note>
