All posts

Building One MCP App for ChatGPT and Claude

Abe Wheeler
MCP Apps MCP App Framework ChatGPT Apps ChatGPT App Framework Claude Apps MCP App Testing ChatGPT App Testing
Building one app for ChatGPT and Claude!

Building one app for ChatGPT and Claude!

TL;DR: ChatGPT and Claude both support MCP Apps. Build with the core sunpeak API, and the same React component runs on both hosts without changes. One codebase, one test suite, two+ app stores.

ChatGPT launched MCP App support on February 4, 2026. Claude launched it on January 26, 2026. Both hosts render your React components inside sandboxed iframes in the chat. Both pass tool output to your component through the same protocol.

This means if you have already built for one host, your app works on the other. And if you are starting fresh, you can ship to both from day one.

One Protocol, Multiple Hosts

MCP Apps extend the Model Context Protocol with interactive UI. An MCP App has two parts: a tool (MCP server) and a resource (React component). The tool handles backend logic. The resource renders the result as UI. The protocol defines the contract between them.

Because the protocol is the same everywhere, your resource code has no host-specific dependencies. Here is a weather card that runs on ChatGPT and Claude without changes:

import { useToolData, SafeArea } from 'sunpeak';
import type { ResourceConfig } from 'sunpeak';

export const resource: ResourceConfig = {
  name: 'weather-card',
  description: 'Display current weather conditions',
};

interface WeatherData {
  city: string;
  temp: number;
  condition: string;
  humidity: number;
  wind: string;
}

export function WeatherResource() {
  const { output } = useToolData<unknown, WeatherData>(undefined, undefined);
  if (!output) return null;

  return (
    <SafeArea className="p-6 font-sans max-w-sm mx-auto">
      <h1 className="text-2xl font-bold">{output.city}</h1>
      <div className="text-5xl font-light my-4">{output.temp}°F</div>
      <div className="text-gray-500 mb-4">{output.condition}</div>
      <div className="flex gap-6 text-sm">
        <div>
          <div className="text-gray-400">Humidity</div>
          <div className="font-medium">{output.humidity}%</div>
        </div>
        <div>
          <div className="text-gray-400">Wind</div>
          <div className="font-medium">{output.wind}</div>
        </div>
      </div>
    </SafeArea>
  );
}

Every import here comes from sunpeak, not sunpeak/chatgpt or any host-specific path. useToolData receives the tool output. SafeArea handles safe rendering across hosts. ResourceConfig describes the resource to the host. This code runs on ChatGPT, Claude, Goose, VS Code Insiders, and any future host that implements MCP Apps.

What the Core API Covers

The portable sunpeak import gives you everything most apps need:

  • useToolData<TInput, TOutput>() receives the structured data from the tool call. This is how your component gets data from the model.
  • useAppState<T>() manages state that syncs back to the host. When the user checks a box or fills a field, the model knows about it.
  • useDisplayMode() reports whether the app is rendering inline, fullscreen, or picture-in-picture. You can adapt your layout accordingly.
  • useHostContext() provides metadata about the host environment (theme, locale, viewport).
  • SafeArea handles safe rendering boundaries so your component respects the host’s chrome and padding.
  • ResourceConfig describes your resource (name, description, MIME type) so the host knows what it is.

None of these reference a specific host. They map directly to MCP App protocol primitives.

When You Might Need Host-Specific Code

Rarely. But sunpeak provides opt-in subpath imports for cases where you do:

// Host-specific (ChatGPT only)
import { ChatGPTSimulator } from 'sunpeak/chatgpt';

The sunpeak/chatgpt subpath exports the ChatGPT-specific simulator and functionality.

Your resource components should stick to the core sunpeak import as much as possible. If a component imports from a host-specific subpath, it is no longer portable by definition. The code path that uses these imports should be gated behind a conditional check and the app should be able to function whether called by that host or not.

import { isChatGPT } from 'sunpeak';

// Check platform at runtime
if (isChatGPT()) {
  // Running in ChatGPT
}

In practice, most apps never need host-specific code. The core API handles data flow, state management, display modes, and theming for all hosts.

Testing Across Hosts

One of the advantages of a shared protocol is that you only need one test suite.

The sunpeak simulator on localhost:3000 replicates the MCP App runtime for both hosts. Your simulation files (JSON mock data in tests/simulations/) feed the same data your component would receive from ChatGPT or Claude in production.

{
  "userMessage": "What's the weather in Austin, Texas?",
  "tool": {
    "name": "show-weather",
    "description": "Show the weather in any city",
    "inputSchema": {
      // ...
    },
    "title": "Show Weather",
    "annotations": { "readOnlyHint": true },
    "_meta": {
      "ui": { "visibility": ["model", "app"] }
    }
  },
  "toolInput": {
    "city": "Austin",
    "state": "Texas"
  },
  "toolResult": {
    "structuredContent": {
      "city": "Austin",
      "temp": 82,
      "condition": "Sunny",
      "humidity": 78,
      "wind": "12 mph W"
    }
  }
}

Unit tests with Vitest verify your component logic. End-to-end tests with Playwright verify the rendered output. Both test frameworks come pre-configured when you scaffold with sunpeak new.

The testing guide covers this in detail: mocking tool data, testing display modes, testing dark mode, and writing end-to-end tests that verify your component renders correctly.

Because the test environment is host-agnostic, passing tests mean your app works on every host. You do not need separate test runs for ChatGPT and Claude.

Project Structure

A sunpeak project that targets both hosts looks like this:

my-app/
├── src/
│   └── resources/
│       └── weather/
│           └── weather-resource.tsx        # your component (portable)
├── tests/
│   └── simulations/
│       └── weather/
│           └── sunny-day-simulation.json   # mock data (shared)
└── package.json

Sunpeak auto-discovers resources by directory convention: src/resources/{name}/{name}-resource.tsx. Simulation files map to resources by matching directory names under tests/simulations/.

There is nothing host-specific in this structure. No chatgpt/ or claude/ directories. One resource, one simulation directory, one build.

When you run sunpeak build, the output is a single production bundle that any MCP App host can load.

From Zero to Both Hosts

If you are starting a new project:

pnpm add -g sunpeak
sunpeak new
sunpeak dev

The ChatGPT App tutorial walks through building a resource from scratch, adding simulation data, and running the simulator. Every step applies to Claude too because the same code powers both.

If you already have a ChatGPT App, it already runs on Claude. If you already have a Claude App, it already runs on ChatGPT. There is no migration step because there is nothing host-specific to migrate.

The real win is what comes next. When a new host adopts MCP Apps, your app runs there too, with zero additional work. You are not building for two platforms. You are building for a protocol.

Resources

Build once with sunpeak. Run on ChatGPT, Claude, and every MCP App host.

Frequently Asked Questions

Can the same MCP App run on both ChatGPT and Claude?

Yes. ChatGPT and Claude both implement the MCP App standard. An MCP App built with sunpeak runs on both hosts without code changes. The core API (useToolData, useAppState, SafeArea, useDisplayMode) is host-agnostic, so your React component renders identically in both environments.

What is the MCP App standard?

MCP Apps are an extension of the Model Context Protocol (MCP) that adds interactive UI to MCP tools. Instead of returning plain text, a tool can return a resource that renders HTML in an iframe (often a built React component) in the host. The standard defines how tool output reaches the component, how the component communicates state back, and how the host handles display modes and theming.

Do I need separate codebases for ChatGPT and Claude?

No. A single sunpeak project produces one build that runs on both hosts. Your resource components, simulation files, and test suites are shared. If you need host-specific behavior (rare), sunpeak provides opt-in subpath imports like sunpeak/chatgpt. The core sunpeak import stays portable.

How do I test my MCP App for both ChatGPT and Claude?

Run sunpeak dev to start the local simulator. The simulator replicates both ChatGPT and Claude App runtimes, so you can test your component against both hosts on localhost. For automated testing, use Vitest for unit tests and Playwright for end-to-end tests. The same test suite covers both hosts.

What hosts support MCP Apps besides ChatGPT and Claude?

As of February 2026, MCP App hosts include ChatGPT (launched February 4, 2026), Claude (launched January 26, 2026), Goose (Block open-source AI agent), and VS Code Insiders. More hosts are expected to adopt the standard because MCP is an open protocol.

What is sunpeak?

Sunpeak is an open-source MCP App framework for building interactive apps that run inside AI hosts like ChatGPT and Claude. It includes a local simulator, pre-built UI components, TypeScript and React project scaffolding, and testing utilities with Vitest and Playwright. Install with pnpm add -g sunpeak.

What parts of the sunpeak API are host-specific?

The core sunpeak import (useToolData, useAppState, useDisplayMode, useHostContext, SafeArea, ResourceConfig) is fully host-agnostic. Host-specific utilities live under subpath imports like sunpeak/chatgpt, which exports ChatGPTSimulator and buildDevSimulations. Most apps never need host-specific code.