Integration Testing for MCP Apps, ChatGPT Apps, and Claude Connectors (September 2026)

Integration testing MCP App tool handlers and resource components together.
An MCP App can pass every component test and still fail before its UI appears. The host may discover the wrong metadata, the tool may return data that does not match its schema, the ui:// resource may have the wrong MIME type, or a retry may create the same record twice.
Integration tests find those failures by crossing the real boundaries in your app. They call the MCP server instead of importing a handler, read the same resource a host reads, and check the same result the model and View receive.
TL;DR: Test MCP Apps in layers. First test discovery, tool calls, result channels, resources, auth, errors, retries, and multi-tool workflows through a real MCP server. Run the official conformance suite separately for every protocol revision you claim. Then render real server results in a local host runtime, run model evals for tool selection, and keep a small live suite for the deployed ChatGPT or Claude connection.
What an Integration Test Proves
An integration test should make a narrow, named claim about two or more parts working together. For an MCP App, useful claims include:
- The MCP endpoint discovers a tool with the expected schema and metadata.
- A valid
tools/callreaches the handler and returns the documented result. structuredContentmatchesoutputSchemaat runtime.- The tool descriptor points to a readable
ui://resource. - The resource has
text/html;profile=mcp-app, a tight CSP, and the expected HTML bundle. - An authenticated user can read an allowed object but cannot read another tenant’s object.
- A retry with the same idempotency key does not repeat a destructive write.
- One tool’s output supplies a valid ID to the next tool in a workflow.
The test needs two oracles: the public contract and the observable effect. If create_order returns an order ID, check the result shape and the stored order. A snapshot of the JSON response alone can stay green while the database write fails.
This table separates common test claims:
| Layer | Boundary under test | Best assertion |
|---|---|---|
| Unit | One function, schema helper, or View state branch | Return value or rendered branch |
| Integration | MCP endpoint, handler, resource, auth, and owned storage | Public contract plus observable effect |
| Conformance | Client or server against a dated MCP specification | Official scenario result and wire checks |
| E2E | Tool result through a host runtime and iframe | User-visible behavior and bridge calls |
| Eval | Model prompt to tool choice and arguments | Repeated pass rate over a fixed prompt set |
| Live | Deployed endpoint through a production host | Connection, auth, tool call, and rendered UI |
Calling all of these tests “E2E” makes failures harder to place. Name the boundary in the test title and keep the assertion close to that boundary.
Start With the Five MCP App Gates
A tool-backed View passes through five gates before a user can interact with it:
- Discovery: The host learns the tool name, description, schemas, annotations, UI resource URI, and visibility.
- Execution: The server validates input, authorizes the user, runs the handler, and maps the result.
- Resource delivery: The host reads the linked
ui://resource and accepts its MIME type, CSP, permissions, and HTML. - View lifecycle: The iframe connects to the host, receives tool input and result notifications, and handles host context.
- Follow-up work: The View calls another server tool, updates model context, or continues a workflow with a stable ID.
Protocol integration tests own the first three gates and the server half of the fifth. Browser E2E tests own the View lifecycle and the client half of follow-up work. Real-host tests confirm the deployed path.
This division gives you a useful debug order. If a View is blank, inspect discovery and the tool result before changing CSS.
Call the Real MCP Server
In a sunpeak project, the mcp fixture from sunpeak/test calls the configured server through MCP. It does not import your handler directly, so transport parsing, schema validation, handler code, and result serialization stay in the test.
import { expect, test } from 'sunpeak/test';
test('search exposes a complete tool-to-View contract', async ({ mcp }) => {
const tools = await mcp.listTools();
const search = tools.find((tool) => tool.name === 'search_products');
expect(search).toMatchObject({
name: 'search_products',
annotations: { readOnlyHint: true },
_meta: {
ui: {
resourceUri: 'ui://catalog/search-results',
visibility: ['model', 'app'],
},
},
});
const result = await mcp.callTool('search_products', {
query: 'headphones',
});
expect(result.isError).toBeFalsy();
await expect(result).toHaveStructuredContent({
results: expect.any(Array),
total: expect.any(Number),
});
const html = await mcp.readResource('ui://catalog/search-results');
expect(html).toContain('<!doctype html>');
});
For an existing MCP server, scaffold the test harness around the endpoint you already run:
npx sunpeak test init --server http://localhost:8000/mcp
npx sunpeak test
The server can be written in TypeScript, Python, Go, Rust, or another language. The target is the MCP contract, so the test client does not need to share the server’s stack.
Test Discovery as a Product Contract
Tool metadata changes model behavior and host behavior, so discovery deserves direct tests. Check each field that carries meaning:
name,title, anddescriptionidentify when the tool should run.inputSchemadefines what the host may send.outputSchemadefines the JSON shape callers can depend on.readOnlyHint,destructiveHint,idempotentHint, andopenWorldHintdescribe behavior and risk._meta.ui.resourceUrilinks the tool to its View._meta.ui.visibilitysays whether the model, the App, or both can call the tool.
Do not snapshot the entire tool list and stop there. Whole-object snapshots are noisy, and developers often update them without reading the diff. Add focused assertions for fields whose wrong value would change routing, confirmation, security, or rendering.
Also include a metadata eval set. OpenAI’s current plugin testing guide recommends direct requests, indirect requests, follow-ups that reuse earlier identifiers, write actions that need authorization or confirmation, and requests that should not call a tool. Rerun those prompts when names, descriptions, schemas, or annotations change.
Validate outputSchema and structuredContent Together
The current MCP tools contract can return content, structuredContent, isError, and _meta. For an MCP App, each field has a different reader:
| Result field | Main reader | Integration test |
|---|---|---|
content | Model and non-UI clients | Useful text or media fallback |
structuredContent | Model and View | Runtime value matches outputSchema |
isError | Host, model, and View | Expected failures use a usable result |
_meta | Host and View | Required UI data exists, secrets do not |
Compile-time types cannot prove the value that crossed the MCP boundary. Validate the actual result, including empty and partial data:
test('search result matches its declared runtime contract', async ({ mcp }) => {
const result = await mcp.callTool('search_products', {
query: 'headphones',
limit: 10,
});
expect(result.content).toEqual([{ type: 'text', text: expect.stringContaining('products') }]);
expect(result.structuredContent).toMatchObject({
results: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
name: expect.any(String),
priceCents: expect.any(Number),
}),
]),
total: expect.any(Number),
});
expect(result.structuredContent).not.toHaveProperty('accessToken');
expect(result._meta).not.toHaveProperty('rawCustomerRecord');
});
Treat _meta as hidden from the model, not as secret storage. The host and View can receive it, so add negative assertions for tokens, credentials, unrelated tenant IDs, and raw upstream records.
Read the UI Resource in the Same Test
The tool result can be correct while the View link is broken. Test the descriptor and resource as one contract:
- Find the tool through
tools/list. - Read
_meta.ui.resourceUri. - Fetch that URI through
resources/read. - Verify the MCP App HTML MIME type.
- Check the CSP and permission allowlists.
- Confirm the returned HTML contains the production entry point.
Use the stable MCP Apps specification for metadata names and bridge behavior. The official basic-host reference app is also useful for manual checks because it shows tool input, tool result, messages, model-context updates, and host lifecycle logs.
Keep one browser test that renders a real server result rather than a simulation. Simulations are useful for empty, error, large-data, and rare states, but they can drift after a handler changes. A real-server render ties the protocol contract to the View.
Test the 2026-07-28 Stateless Lifecycle
The MCP 2026-07-28 core removed the initialize handshake and Mcp-Session-Id. Each request is self-contained and carries its protocol version, client information, and client capabilities. A client may call server/discover, but discovery is optional.
That change creates integration cases older suites can miss:
- A tool call succeeds on a fresh connection with no prior handshake.
- Two consecutive requests can reach different server instances and still continue the workflow.
- Application state travels through an explicit record, workflow, or task handle.
MCP-Protocol-Version,Mcp-Method, andMcp-Nameagree with the request body on Streamable HTTP.- A cached list result respects its advertised lifetime and invalidation path.
- Capability-dependent behavior uses the capabilities on the current request.
Run the official conformance suite for the frozen requirements of the revision:
npx @modelcontextprotocol/conformance server \
--url http://localhost:8000/mcp \
--requirements 2026-07-28
Conformance and app integration tests answer different questions. Conformance checks the dated protocol rules. Your tests still need to check tool meaning, authorization, business side effects, UI metadata, and the data your View reads.
If the server supports an older MCP revision too, test that revision at its own lifecycle. Passing a scenario on the stateless wire does not prove the stateful wire works.
Build a Negative Test Matrix
Most production bugs live outside the success fixture. Build a matrix by boundary instead of adding random edge cases:
| Boundary | Cases worth testing |
|---|---|
| Input | Missing required field, wrong type, extra field, oversized value, invalid enum |
| Auth | Missing token, expired token, wrong scope, wrong tenant, revoked account |
| Upstream | Timeout, rate limit, empty result, partial record, malformed record |
| Result | Schema mismatch, missing fallback content, leaked secret, isError omitted |
| Resource | Missing URI, wrong MIME type, stale bundle, CSP block, unreadable resource |
| Workflow | Stale revision, unknown handle, expired handle, changed field name |
| Mutation | Duplicate request, retry after timeout, concurrent write, failed confirmation |
| Capability | Method unsupported, display mode declined, app-only tool called by model |
Expected tool failures should return isError: true with useful content. Protocol errors are for malformed or unsupported MCP requests. Keeping those branches distinct helps the host and makes failures easier to diagnose.
For destructive operations, assert on what did not happen:
import { countOrders } from '../helpers/test-database';
test('invalid quantity never creates an order', async ({ mcp }) => {
const result = await mcp.callTool('create_order', {
productId: 'sku_123',
quantity: -5,
idempotencyKey: 'test-invalid-order',
});
expect(result.isError).toBe(true);
expect(result.content[0]).toMatchObject({
type: 'text',
text: expect.stringContaining('quantity'),
});
await expect(countOrders()).resolves.toBe(0);
});
Test Authorization and Tenant Isolation Through MCP
A handler unit test that stubs userId does not prove the endpoint derives identity from a validated credential. Integration tests should enter through the real auth boundary.
For each protected tool, cover:
- no credential
- valid credential with the right scope
- valid credential with the wrong scope
- object owned by another tenant
- expired or revoked credential
- write after authorization changes
Check both the result and the data store. A safe cross-tenant test returns no private fields and leaves the target record unchanged. If the server caches authorization or resource data, repeat the call after revocation so the test covers invalidation.
Use provider-managed OAuth in a smaller staging suite when local fixtures cannot reproduce discovery, consent, refresh, or revocation. Keep those tests separate because they depend on network and account state.
Chain Tools With Real IDs
Many MCP Apps are workflows. A search tool returns an ID, a detail tool reads it, and an action tool changes the object. Test that chain without rewriting IDs inside the test:
test('search to detail to order keeps contracts compatible', async ({ mcp }) => {
const search = await mcp.callTool('search_products', {
query: 'headphones',
});
const productId = search.structuredContent.results[0].id;
const detail = await mcp.callTool('get_product', { productId });
expect(detail.structuredContent.id).toBe(productId);
const first = await mcp.callTool('create_order', {
productId,
quantity: 1,
idempotencyKey: 'workflow-order-1',
});
const retry = await mcp.callTool('create_order', {
productId,
quantity: 1,
idempotencyKey: 'workflow-order-1',
});
expect(retry.structuredContent.orderId).toBe(first.structuredContent.orderId);
});
This catches field-name drift and retry bugs that isolated handler tests miss. For the stateless core, it also proves the workflow depends on explicit IDs rather than a hidden transport session.
Mock at a Boundary You Can Name
Keep the real MCP server, schemas, handler mapping, result formatting, and owned storage in the broad integration suite. Replace dependencies that make the test slow or unstable:
- external APIs
- email and payment delivery
- third-party webhooks
- model calls
- wall-clock time
Mock at the client or network adapter, then state that boundary in the test. “Returns a useful MCP error when the catalog API times out” is more useful than “handles errors.”
Add a smaller staging suite for facts the mock cannot prove: OAuth refresh, upstream pagination, live rate-limit headers, webhook signatures, and provider schema changes. Track fixtures against a recorded or validated upstream contract so mocks do not become a second invented API.
Split CI by Cost and Ownership
A fast pull-request job should run deterministic tests that your team can fix from the repository:
pull request
unit tests
MCP contract and workflow tests
auth and tenant-isolation tests
official conformance requirements
production UI bundle build
local host-runtime browser tests
release or schedule
staging provider integration tests
repeated model tool-selection evals
live ChatGPT and Claude smoke tests
OpenAI’s current plugin guide follows the same broad sequence: test the MCP server first, inspect tools directly, exercise representative and edge-case inputs, then connect the server in developer mode. Before packaging, it asks developers to verify tool metadata, output-schema matches, useful auth errors, positive and negative prompts, console-free UI rendering, and state restoration.
Keep evaluation prompts and results across releases. A metadata change may improve one direct prompt while breaking an indirect request or causing a tool to run when it should not.
Know What Local Integration Tests Cannot Prove
A green local suite does not prove that a hosted client can reach your production server. It also does not prove current workspace policy, OAuth consent, confirmation UI, tool caching, or iframe behavior in a live host.
Finish a release with a narrow live checklist:
- Connect the deployed HTTPS MCP endpoint.
- Refresh tool metadata and inspect the discovered fields.
- Run one direct prompt and one indirect prompt.
- Complete one authenticated read and one confirmed write, if the app supports writes.
- Verify the UI has no console errors and restores the intended state.
- Run one request that should not select the app.
Those live checks should stay small. Put broad input, error, workflow, and layout coverage in deterministic local tests so a hosted-client outage does not block every pull request.
Get Started
If you already use sunpeak, add one test with the mcp fixture for your most important tool. Assert its discovery metadata, call it, validate the result channels, and read the linked UI resource. Then render that real result in the sunpeak MCP App Inspector.
For an existing server, add the same test stack without changing the server framework:
npx sunpeak test init --server http://localhost:8000/mcp
npx sunpeak test
The sunpeak testing framework keeps protocol tests and host-runtime browser tests in one project. Keep the claim for each test narrow, run dated MCP conformance separately, and use live hosts only for the parts a local runtime cannot prove.
Get Started
npx sunpeak newFurther Reading
- Complete guide to testing ChatGPT Apps and MCP Apps
- MCP App E2E testing with Playwright and host replicas
- MCP App conformance testing
- MCP App testing strategy
- Tool results: content, structuredContent, and _meta
- MCP App error handling and cancellation states
- MCP App authentication and OAuth
- sunpeak MCP App testing framework
- sunpeak MCP App Inspector
- sunpeak testing documentation
- Official MCP Apps testing guide
- Stable MCP Apps specification (2026-01-26)
- Official MCP conformance suite
- OpenAI guide to connecting and testing a plugin
- MCP 2026-07-28 specification release
Frequently Asked Questions
What is integration testing for MCP Apps?
MCP App integration testing calls a real MCP server through the protocol and checks the contracts that connect tools, results, UI resources, authorization, and downstream services. A useful test can discover a tool, call it, validate content and structuredContent, read its ui:// resource, and confirm the result has everything the View needs. It catches boundary bugs without requiring a model or a hosted chat client for every test run.
What is the difference between MCP App unit, integration, conformance, E2E, and eval tests?
Unit tests isolate a function or component. Integration tests cross boundaries you own, such as MCP transport to handler to database adapter. Conformance tests check the implementation against a dated MCP wire specification. E2E tests render and operate the View in a host runtime. Evals measure whether a model selects the right tool and arguments. Each layer makes a different claim, so name the boundary and expected result in every test.
How should I test an MCP 2026-07-28 server?
Test the stateless lifecycle your server claims to support. Each 2026-07-28 request carries the protocol version, client identity, and capabilities instead of depending on initialize or Mcp-Session-Id. Run the official conformance suite with --requirements 2026-07-28, then add product tests for your tools, auth, result channels, UI resources, and workflows. If you also support an older revision, run that revision at its own wire lifecycle.
What should an MCP App integration test assert?
Assert the public contract and the user-visible effect. Check tool discovery, input and output schemas, annotations, resource URI, UI visibility, content, structuredContent, isError, required result _meta, resource MIME type, CSP, authorization, and durable side effects. Add negative checks for secrets, cross-tenant data, duplicate writes, stale revisions, invalid input, and unsupported capabilities.
How do I test the contract between an MCP tool and its View?
Read the tool descriptor, call the tool through MCP, and fetch the linked ui:// resource. Verify that _meta.ui.resourceUri points to a readable resource with the MCP App HTML MIME type, structuredContent matches the declared outputSchema, and any View-only result _meta is present. Then render at least one real server result in a host replica so a simulation file cannot hide contract drift.
Can I integration test ChatGPT Apps without a paid ChatGPT account?
Yes. Protocol tests and local host-runtime tests do not need a paid host account or model credits. sunpeak can connect to an MCP server in any language, call its tools, read resources, and render Views in local ChatGPT and Claude host replicas. Keep a small live test set for release because local tests cannot prove that a deployed endpoint, current account policy, OAuth flow, or production host still works.
Should MCP App integration tests mock external APIs?
Mock an external API at the network or client boundary for the broad pull-request suite so tests stay deterministic. Keep the real MCP server, schemas, handlers, result mapping, auth checks, and database behavior in scope. Add a smaller staging suite for provider OAuth, pagination, rate limits, webhooks, and upstream schema changes. A test should state which boundary is real and which one is replaced.
Which MCP App integration tests should run in CI?
Run deterministic discovery, tool-call, resource, error, auth, tenant-isolation, workflow, retry, and idempotency tests on every pull request. Also build and test the production UI resource bundle. Put changing external APIs, repeated model evals, and real ChatGPT or Claude connections in scheduled or release jobs, then keep their failures separate from the fast local suite.