Skip to main content
All posts

MCP App Testing Strategy: Which Tests to Write First and What to Skip (August 2026)

Abe Wheeler
MCP AppsMCP App TestingMCP App FrameworkChatGPT AppsChatGPT App TestingChatGPT App FrameworkClaude ConnectorsClaude Connector TestingClaude Connector FrameworkTesting StrategyTesting Best Practices
A practical testing strategy for MCP Apps across ChatGPT and Claude.

A practical testing strategy for MCP Apps across ChatGPT and Claude.

TL;DR: Test MCP Apps by boundary, not by a generic test pyramid. Start with discovery and tool-result contracts, then pin important UI states and render them in each host runtime you support. Test the production bundle before release. Keep live-host checks and model evals small and targeted because they are slower, cost money, and change outside your repository.


An MCP App can fail even when its React component and tool handler both work in isolation. The host must discover the tool, follow its ui:// resource link, read valid HTML, deliver the tool result to a sandboxed iframe, and carry app requests back across the bridge. Authentication, Content Security Policy, CORS, host capabilities, and model tool selection add more failure points.

The MCP Apps specification defines the shared contract, but MCP Apps are an optional extension and host support varies. That makes a fixed unit, integration, and E2E pyramid a weak plan. A useful strategy maps tests to the boundaries your app crosses and the claims you make to users.

Build the Strategy from Risk

Score each feature on three questions:

  1. How much damage does a failure cause?
  2. How likely is that failure after an ordinary code or dependency change?
  3. What is the cheapest test that reports the real cause?

A read-only chart and a payment approval flow should not have the same suite. The chart may need broad viewport and visual coverage. The approval flow needs authorization, confirmation, idempotency, retry, and audit checks before screenshot coverage matters.

Use this default schedule as a starting point:

Test layerWhat it protectsWhen to run
Static and protocol contractsDiscovery, schemas, metadata, tool results, resourcesEvery pull request
Unit testsBranching rules, transforms, reducers, validation helpersEvery pull request
Deterministic inspector testsUI states, app actions, host context, sandbox behaviorEvery pull request
Production-resource testsBundled HTML, CSP, assets, source maps, environment driftEvery pull request or release candidate
Visual and accessibility testsLayout, responsive behavior, keyboard and semantic UIUI changes and release candidates
Live-host smoke testsReal connection, model invocation, host rendering, OAuthMain, nightly, or release candidate
Model evalsTool choice, arguments, sequences, app-context follow-upsMain, nightly, or metadata changes

The lower rows are not more valuable. They are less deterministic and more expensive, so run fewer of them.

1. Test Discovery Before Rendering

The first test should prove that a host can discover your app. Check tools/list, resources/list, and resources/read before opening a browser.

For every UI tool, assert that:

  • Its name, description, input schema, annotations, and security schemes are present.
  • _meta.ui.resourceUri points to the intended ui:// resource.
  • A tool that returns structuredContent declares an exact outputSchema when the target host expects one. OpenAI’s current plugin reference requires this pairing.
  • The resource is discoverable and returns text/html;profile=mcp-app.
  • Security and rendering metadata appear on the resource content returned by resources/read, where the standard defines them.

With sunpeak, the mcp Playwright fixture exercises the running server rather than an imported handler:

import { test, expect } from 'sunpeak/test';

test('search tool exposes a valid app contract', async ({ mcp }) => {
  const tools = await mcp.listTools();
  const search = tools.find((tool) => tool.name === 'search-catalog');

  expect(search).toBeDefined();
  expect(search?._meta?.ui?.resourceUri).toBe('ui://catalog/search');
  expect(search?.inputSchema.properties).toHaveProperty('query');
  expect(search?.outputSchema.properties).toHaveProperty('items');

  const resources = await mcp.listResources();
  const resource = resources.find((item) => item.uri === 'ui://catalog/search');
  expect(resource?.mimeType).toBe('text/html;profile=mcp-app');
});

This test catches a missing resource registration, stale URI, wrong MIME type, and schema drift in seconds. A component test cannot see any of those failures.

2. Test Tool Results as Public Contracts

Call every tool through MCP with a normal input, an edge input, and an invalid input. Assert behavior, not just object shape.

test('search returns model and app data that match the schema', async ({ mcp }) => {
  const result = await mcp.callTool('search-catalog', {
    query: 'standing desk',
  });

  expect(result.isError).toBeFalsy();
  expect(result.content).toEqual(
    expect.arrayContaining([expect.objectContaining({ type: 'text' })])
  );
  expect(result.structuredContent).toMatchObject({
    items: expect.any(Array),
  });
});

Cover these result paths when they exist:

  • Valid results, empty results, pagination, and partial upstream data.
  • Input-schema rejection before side effects begin.
  • Domain errors returned in a form the model and app can explain.
  • OAuth challenges and expired sessions.
  • Timeouts, rate limits, retries, and cancellation.
  • Oversized or malformed upstream responses.

Tools that change data need extra checks. Verify authorization on the server, user confirmation at the right step, idempotency under retries, duplicate-call handling, rollback or partial-failure behavior, and accurate tool annotations. Treat annotations as host hints, not access control. The server must enforce the rule even when a client ignores a hint.

3. Pin UI States with Deterministic Fixtures

Live data is poor input for most UI tests because records, timing, permissions, and upstream services change. Save fixtures for the states a user can actually see.

A useful state inventory includes:

  • Initial tool input, streaming or partial input, and final input.
  • Success with typical, empty, long, and partially missing data.
  • Tool error, authentication required, cancelled, and retrying.
  • App-initiated server tool success, rejection, and error.
  • Light and dark themes, supported display modes, narrow and wide viewports.
  • Host capabilities present and absent.
  • App state or model context that survives a follow-up action.

sunpeak simulations store these states as JSON. They let browser tests render the same tool result without calling an API or spending host credits. Keep the fixture close to the contract: include the tool input, tool result, and any server-tool mocks the resource uses.

Do not build a full Cartesian product. Pick pairs that cover each independent risk at least once. For example, test the success state in both supported hosts, the empty state on a narrow viewport, the error state in dark mode, and fullscreen with the densest data. Add a combination only when the interaction between its dimensions can break.

4. Render User Workflows in Host Replicas

Protocol tests prove that data exists. Browser tests prove that a person can use it.

The official MCP Apps overview describes a sandboxed iframe and a JSON-RPC bridge between the app and host. Your browser suite should therefore test through that boundary instead of mounting the React component alone for every case.

test('user can choose a result and continue', async ({ inspector }) => {
  const result = await inspector.renderTool(
    'search-catalog',
    { query: 'standing desk' },
    { theme: 'dark', displayMode: 'inline' }
  );

  expect(result).not.toBeError();
  const app = result.app();

  await expect(app.getByRole('heading', { name: /search results/i })).toBeVisible();
  await app.getByRole('button', { name: /select/i }).first().click();
  await expect(app.getByText(/selected/i)).toBeVisible();
});

Run the same semantic assertions in each host replica you claim to support. Check feature detection and fallbacks rather than hard-coding assumptions about which host always supports a feature. Hosts can add capabilities on different schedules, and the extension support matrix changes over time.

Browser assertions should cover:

  • Visible content and accessible names, not internal component state.
  • Keyboard navigation, focus order, and focus after dialogs or tool calls.
  • No page overflow, clipped controls, or hidden content at supported widths.
  • Tool calls initiated by the app and the state shown while they run.
  • Link, download, display-mode, and model-context requests when used.
  • Console errors, failed requests, and CSP or CORS violations.

Keep focused unit tests for pure transforms, reducers, validation helpers, and branching logic with many cheap cases. A unit test is useful when it gives a faster and clearer failure than the protocol or browser layer. It adds little when it only repeats a browser assertion with mocked hooks.

5. Test the Production Artifact

Development mode can hide production failures. HMR injects scripts and connections, local assets may resolve from the source tree, and development servers can use different origins or environment values.

Before release, build the actual resource and verify:

  1. The MCP server returns bundled HTML rather than a development entry point.
  2. JavaScript, CSS, fonts, images, and source maps resolve as intended.
  3. _meta.ui.csp declares every production connection and resource origin, with no development-only origins.
  4. Browser requests pass CORS from the app iframe origin.
  5. OAuth discovery, callback URLs, cookies, and session expiry work on the deployed endpoint.
  6. A cold load, refresh, remount, and repeated tool call do not depend on stale in-memory state.

sunpeak’s Inspector can load production resources instead of Vite output, so the same Playwright flow can inspect the bundled app. This check belongs before a live-host test because it gives a local, repeatable error when the bundle is wrong.

6. Keep Live-Host Tests Small

A host replica cannot prove that a production host accepts your connection, asks the model to call the intended tool, or renders the current resource exactly as expected. Keep a small smoke suite in real hosts for those risks.

OpenAI’s current connect and test guide recommends testing the MCP endpoint directly first, then checking representative inputs, edge cases, authentication, annotations, confirmation behavior, and model-readable results before testing the installed plugin. That order makes failures easier to diagnose.

A live smoke suite usually needs only a few paths:

  • Connect or refresh the deployed MCP server.
  • Ask a direct prompt that should invoke the primary tool.
  • Confirm the app renders and its main action works.
  • Exercise one authenticated path and one error path.
  • Verify a follow-up prompt can use the expected prior result or app context.

Run live tests on main, nightly, or before a release. They depend on external UI, accounts, sessions, model behavior, and credits, so they are a poor default pull-request gate. sunpeak live tests automate the browser flow while keeping them separate from deterministic Inspector tests.

7. Add Evals for Model Behavior

Evals answer a different question: can the model choose and call your tools reliably?

Add them when tools overlap, schemas are easy to misread, follow-up prompts depend on model-visible app context, or a cheaper model is part of your support target. Test direct requests, paraphrases, requests that should not call a tool, missing arguments, ambiguous intent, and multi-step sequences.

Measure a pass rate over repeated runs rather than trusting one successful prompt. Record the model identifier, tool catalog, prompt set, arguments, latency, and failure reason so a metadata change can be compared with the previous version. Anthropic’s current MCP connector documentation notes that tool selection depends on clear names and descriptions, especially as tool sets grow.

Do not use evals to test deterministic code. A model cannot prove that structuredContent matches outputSchema, that a button is keyboard accessible, or that a production CSP permits an image. sunpeak evals keep model checks separate from protocol and browser tests, and can seed app context for follow-up prompts.

What to Skip

Skip work that produces maintenance without protecting one of your contracts:

  • Re-testing JSON-RPC serialization or SDK bridge internals when you use them without modification. Test your integration with the bridge. If you wrote a custom transport, adapter, or host, that code is yours and needs direct tests.
  • Testing every host, theme, display mode, viewport, locale, permission, and data state together. Use risk-based pairs and add a combination after it causes or could plausibly cause a bug.
  • Snapshotting host-owned conversation chrome. It changes outside your control. Capture your app content and assert the host capability you depend on.
  • Mocking every MCP hook in component tests when a deterministic iframe test already covers the same behavior.
  • Running paid live-host tests and model evals on every commit. Run stable local layers first and schedule external layers where their signal justifies their cost.
  • Treating line coverage as the goal. Cover contracts, side effects, user decisions, security boundaries, and production claims.

A Minimum Suite You Can Ship

For a small read-only MCP App with one UI tool, start with:

  1. A discovery test for the tool, ui:// resource, schemas, annotations, and MIME type.
  2. Tool calls for valid, empty, invalid, and upstream-error inputs.
  3. Deterministic browser tests for success, empty, error, and authentication states.
  4. The main workflow in each supported host replica, plus the modes and widths the UI claims to support.
  5. One production-resource smoke test that checks assets, CSP, CORS, and console errors.
  6. One live-host release check for connection, tool invocation, render, and primary action.

Add mutation checks before any write tool ships. Add visual and accessibility tests when the UI has meaningful interaction or layout risk. Add evals when model tool selection becomes a measured product requirement.

You can scaffold protocol, Inspector, visual, live-host, and eval tests for any MCP server with:

npx sunpeak test init --server http://localhost:8000/mcp
npx sunpeak test

The sunpeak testing framework works with MCP servers written in TypeScript, Python, Go, Rust, or another stack because it connects through MCP. Start with the cheapest boundary that can expose the real failure, then add coverage only when your app’s risks require it.

Get Started

Documentation →
npx sunpeak new

Further Reading

Frequently Asked Questions

What should I test first in an MCP App?

Start at the MCP contract boundary. List the server tools and resources, verify the tool-to-UI resource link, call each tool with one representative input, and validate structuredContent against the declared outputSchema. This catches discovery, schema, serialization, and resource-link failures before browser tests add more variables.

What is the minimum test suite for an MCP App?

A small app needs protocol contract tests for every tool, deterministic browser tests for success, empty, error, and authentication states, coverage for each host and display mode the app claims to support, one production-bundle smoke test, and a small live-host check before release. Add mutation, accessibility, visual, performance, and model eval coverage when the app risk calls for it.

Should MCP App tests follow a testing pyramid?

Use the pyramid as a cost reminder, not a fixed recipe. MCP Apps cross an MCP server, tool result contract, resource bundle, iframe bridge, and host runtime, so protocol integration and browser tests often catch more risk than isolated component tests. Choose each test by failure impact, likelihood, and the cost of getting a useful signal.

How do I test MCP App tool results?

Call the tool through an MCP client, then assert isError, content, structuredContent, and any safe result metadata your app uses. Validate successful output against outputSchema, test invalid input and empty output, and cover OAuth challenges or domain errors. For tools that change data, also test authorization, confirmation, idempotency, retries, and partial failure.

How do I test an MCP App across ChatGPT and Claude?

Run the same user-facing browser assertions in separate host replicas, then keep a smaller set of live-host smoke tests. Cover the themes, display modes, viewport sizes, capabilities, and sandbox rules your app actually uses. Do not assume every host implements the optional MCP Apps extension in the same way or on the same schedule.

When should I add visual regression tests to an MCP App?

Add visual baselines for dense, responsive, branded, or interaction-heavy states once the layout is stable enough to review diffs. Capture the app content rather than volatile host chrome. Keep semantic Playwright assertions too, because a screenshot can look plausible while a control is inaccessible or a tool call is broken.

When does an MCP App need model evals?

Add evals when model behavior is part of the risk: several tools have overlapping intent, arguments are often omitted or confused, or follow-up prompts depend on app context. Evals should measure tool choice, arguments, sequence, and pass rate over repeated runs. They do not replace deterministic server and UI tests.

What MCP App tests can I skip?

Skip tests that duplicate framework or SDK internals, exhaustive Cartesian products of every host, theme, mode, and state, broad snapshots of host-owned UI, and paid live-host runs on every pull request. Also skip component tests that only mirror an existing protocol or browser assertion. Keep a test when it protects one of your own contracts, user actions, security boundaries, or production claims.