All posts

Building One MCP App for ChatGPT and Claude (April 2026)

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 implement the MCP Apps standard. Build with the core sunpeak API and the same React component runs on both hosts (plus Goose, VS Code, Postman, and MCPJam) with no code changes. One codebase, one test suite, every host that speaks the spec.

ChatGPT shipped MCP App support on February 4, 2026. Claude shipped it on January 26, 2026. Both hosts render your React components inside sandboxed iframes in the chat, and both pass tool output to your component through the same JSON-RPC 2.0 protocol over postMessage. Since then the host list has grown to six, and the spec (modelcontextprotocol/ext-apps on GitHub) has stabilized at v1.7.0 under Agentic AI Foundation governance.

If you have already built for one host, your app already works on the others. And if you are starting fresh, you can target the whole ecosystem from day one.

One Protocol, Many Hosts

MCP Apps extend the Model Context Protocol with interactive UI. An MCP App has two parts: a tool (an MCP server endpoint) and a resource (a React component bundled to HTML). The tool runs backend logic. The resource renders the result as UI. The protocol defines the contract between them: how tool output reaches your component, how state flows back, and how the host handles display modes and themes. For the full breakdown of these primitives, see MCP Concepts Explained.

Because the protocol is host-agnostic, your resource code has no host-specific dependencies. Here is a weather card that runs on ChatGPT, Claude, Goose, VS Code, Postman, and MCPJam without changes:

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

export const resource: ResourceConfig = {
  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 is 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 unchanged on every MCP App host that exists today, and it will run on every future host that implements the spec.

The tool that triggers the resource is equally portable:

// src/tools/show-weather.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';

export const tool: AppToolConfig = {
  resource: 'weather',
  title: 'Show Weather',
  description: 'Get the current weather for a city',
  annotations: { readOnlyHint: true, openWorldHint: true },
};

export const schema = {
  city: z.string().describe('City name'),
  state: z.string().describe('State or region'),
};

type Args = z.infer<z.ZodObject<typeof schema>>;

export default async function (args: Args, _extra: ToolHandlerExtra) {
  // Fetch from your weather API
  return {
    structuredContent: {
      city: args.city,
      temp: 82,
      condition: 'Sunny',
      humidity: 78,
      wind: '12 mph W',
    },
  };
}

The tool references the resource by name, validates input with Zod, and imports types from sunpeak/mcp. Tool annotations like readOnlyHint and openWorldHint are part of the MCP spec and let hosts surface trust signals to users. Nothing here is host-specific.

What the Core API Covers

The portable sunpeak import gives you everything most apps need:

  • useToolData<TInput, TOutput>() returns the structured data from the tool call. This is how your component reads what the model produced.
  • useAppState<T>() manages state that syncs back to the host, so the model can reference user interactions in follow-up turns. See Interactive MCP Apps with useAppState.
  • useDisplayMode() reports whether the app is rendering inline, fullscreen, or picture-in-picture. The display mode reference covers the layout adjustments each mode wants.
  • useHostContext() provides metadata about the host environment (theme, locale, viewport).
  • useTheme() exposes the resolved color scheme so styled components match light or dark mode automatically.
  • SafeArea handles safe rendering boundaries that respect each host’s chrome, padding, and inset.
  • 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 Apps protocol primitives. When a new host adopts the spec, your component picks them up for free.

When You Might Need Host-Specific Code

Rarely. But sunpeak provides opt-in subpath imports for the cases where a host offers something special:

import { isChatGPT } from 'sunpeak';
import { useRequestCheckout } from 'sunpeak/chatgpt';

function BuyButton({ sku }: { sku: string }) {
  if (!isChatGPT()) {
    return <a href={`/checkout?sku=${sku}`}>Buy</a>;
  }
  const requestCheckout = useRequestCheckout();
  return <button onClick={() => requestCheckout({ sku })}>Buy</button>;
}

The pattern: gate the host-specific path behind a runtime check (isChatGPT(), isClaude()) and fall back to a portable behavior. Your component still renders correctly on every host, and the host that does support the feature gets the better experience.

The Inspector itself lives at sunpeak/inspector:

import { Inspector } from 'sunpeak/inspector';

In practice, most apps never need any of these. The core API handles data flow, state, display modes, and theming for every host.

Testing Across Hosts

A shared protocol means you only need one test suite.

The sunpeak inspector at localhost:3000 replicates the MCP App runtime for every host. Your simulation files (JSON mock data in tests/simulations/) feed the same shape of data your component would receive from a real ChatGPT or Claude tool call:

{
  "tool": "show-weather",
  "userMessage": "What's the weather in Austin, Texas?",
  "toolInput": {
    "city": "Austin",
    "state": "Texas"
  },
  "toolResult": {
    "structuredContent": {
      "city": "Austin",
      "temp": 82,
      "condition": "Sunny",
      "humidity": 78,
      "wind": "12 mph W"
    }
  }
}

pnpm test runs unit and E2E tests. pnpm test:unit and pnpm test:e2e run them separately. pnpm test:visual adds visual regression checks. pnpm test:live validates against the real ChatGPT runtime when you are ready. All of these come pre-configured when you scaffold with npx sunpeak new.

The complete testing guide covers the full flow: mocking tool data, testing display modes, testing dark mode, snapshot testing, and writing E2E tests with the inspector fixture. The E2E testing post goes deeper on cross-host fixtures.

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

Project Structure

A sunpeak project that targets every host looks like this:

sunpeak-app/
├── src/
│   ├── resources/
│   │   └── weather/
│   │       └── weather.tsx                # your component (portable)
│   └── tools/
│       └── show-weather.ts                # tool metadata + handler
├── tests/
│   └── simulations/
│       └── show-weather.json              # mock data (shared)
└── package.json

sunpeak auto-discovers resources from src/resources/{name}/{name}.tsx, tools from src/tools/*.ts, and simulations from tests/simulations/*.json. There is nothing host-specific in this layout. No chatgpt/ or claude/ directories. One resource, one simulation directory, one build.

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

From Zero to Every Host

If you are starting a new project:

npx sunpeak new sunpeak-app
cd sunpeak-app
pnpm dev

The MCP App tutorial walks through building a resource from scratch, adding simulation data, and running the inspector. Every step applies to ChatGPT, Claude, and the rest because the same code powers them all. The ChatGPT App tutorial and Claude App guide cover the host-side details when you are ready to ship.

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

The 2026 MCP roadmap commits to vendor-neutral evolution under the Agentic AI Foundation. When the next host adopts MCP Apps, your app runs there too with zero additional work. Building against the protocol means each new host is free distribution.

Get Started

Documentation →
npx sunpeak new

Further Reading

Frequently Asked Questions

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

Yes. ChatGPT and Claude both implement the MCP Apps open standard (ext-apps v1.7.0). An MCP App built with sunpeak runs on both hosts without code changes. The core API (useToolData, useAppState, useDisplayMode, useHostContext, and SafeArea) is host-agnostic, so your React component renders identically across hosts. The same build also runs on Goose, VS Code (via GitHub Copilot), Postman, and MCPJam.

What is the MCP Apps standard?

MCP Apps is the official UI extension to the Model Context Protocol, defined in the ext-apps repo on GitHub. It adds an interactive layer to MCP tools: instead of returning plain text, a tool can declare a ui:// resource that the host renders in a sandboxed iframe. The spec defines the iframe contract, JSON-RPC 2.0 postMessage transport, the host context API, and how display modes and themes work. As of April 2026 the spec is at v1.7.0 with over 2,100 GitHub stars and is governed by the Agentic AI Foundation under the Linux Foundation.

Do I need separate codebases for ChatGPT and Claude?

No. A single sunpeak project produces one bundle that runs on every MCP App host. Your resource components, simulation files, and test suites are shared. The core sunpeak import is host-agnostic. If you need a host-specific feature like ChatGPT checkout or modal requests, sunpeak provides opt-in subpath imports under sunpeak/chatgpt that stay isolated from the portable core.

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

Run pnpm dev to start the sunpeak inspector at localhost:3000. The inspector replicates the ChatGPT and Claude App runtimes, so you can test display modes (inline, fullscreen, picture-in-picture), light and dark themes, and viewport sizes against both hosts on localhost. For automation, pnpm test runs unit and E2E suites, pnpm test:visual runs visual regression checks, and pnpm test:live validates against the real ChatGPT runtime. The same test suite covers every host that implements the spec.

Which hosts support MCP Apps in April 2026?

Six hosts render MCP App UI as of April 2026: ChatGPT (launched Feb 4, 2026), Claude web and desktop (launched Jan 26, 2026), Goose (Block), VS Code via GitHub Copilot, Postman, and MCPJam. The wider MCP ecosystem is much larger (300+ clients speak the protocol), but those six are the confirmed hosts that render interactive App UI in a sandboxed iframe. More are expected because the standard is open and vendor-neutral.

What is sunpeak?

sunpeak is an open-source (MIT) MCP App framework for building, testing, and shipping interactive apps that run inside AI hosts. It includes a local inspector that replicates the ChatGPT and Claude runtimes, an MCP dev server with HMR, pre-built UI components, 20+ typed React hooks, and a built-in testing framework covering unit, E2E, visual regression, and live-host tests. Get started with npx sunpeak new.

What parts of the sunpeak API are host-specific?

The core sunpeak import (useToolData, useAppState, useDisplayMode, useHostContext, SafeArea, ResourceConfig) is fully host-agnostic and maps directly to the MCP Apps protocol. Host-specific utilities live under subpath imports: sunpeak/chatgpt for ChatGPT-only hooks like useUploadFile, useRequestCheckout, and useRequestModal. The Inspector component lives under sunpeak/inspector, and test fixtures under sunpeak/test. Most apps never import anything outside the core.

What changed for cross-host MCP Apps between February and April 2026?

The MCP Apps spec stabilized at v1.7.0, the host list grew from four to six confirmed hosts (adding Postman and MCPJam), and Anthropic donated MCP to the Agentic AI Foundation in April 2026 under Linux Foundation governance. The portable core API has not changed, so apps built earlier still run on every new host without modification.