Skip to main content
All posts

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

Abe Wheeler
MCP AppsMCP App TestingMCP App FrameworkChatGPT AppsChatGPT App TestingChatGPT App FrameworkClaude ConnectorsClaude Connector TestingClaude Connector FrameworkIntegration Testing
Integration testing MCP App tool handlers and resource components together.

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/call reaches the handler and returns the documented result.
  • structuredContent matches outputSchema at 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:

LayerBoundary under testBest assertion
UnitOne function, schema helper, or View state branchReturn value or rendered branch
IntegrationMCP endpoint, handler, resource, auth, and owned storagePublic contract plus observable effect
ConformanceClient or server against a dated MCP specificationOfficial scenario result and wire checks
E2ETool result through a host runtime and iframeUser-visible behavior and bridge calls
EvalModel prompt to tool choice and argumentsRepeated pass rate over a fixed prompt set
LiveDeployed endpoint through a production hostConnection, 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:

  1. Discovery: The host learns the tool name, description, schemas, annotations, UI resource URI, and visibility.
  2. Execution: The server validates input, authorizes the user, runs the handler, and maps the result.
  3. Resource delivery: The host reads the linked ui:// resource and accepts its MIME type, CSP, permissions, and HTML.
  4. View lifecycle: The iframe connects to the host, receives tool input and result notifications, and handles host context.
  5. 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, and description identify when the tool should run.
  • inputSchema defines what the host may send.
  • outputSchema defines the JSON shape callers can depend on.
  • readOnlyHint, destructiveHint, idempotentHint, and openWorldHint describe behavior and risk.
  • _meta.ui.resourceUri links the tool to its View.
  • _meta.ui.visibility says 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 fieldMain readerIntegration test
contentModel and non-UI clientsUseful text or media fallback
structuredContentModel and ViewRuntime value matches outputSchema
isErrorHost, model, and ViewExpected failures use a usable result
_metaHost and ViewRequired 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:

  1. Find the tool through tools/list.
  2. Read _meta.ui.resourceUri.
  3. Fetch that URI through resources/read.
  4. Verify the MCP App HTML MIME type.
  5. Check the CSP and permission allowlists.
  6. 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, and Mcp-Name agree 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:

BoundaryCases worth testing
InputMissing required field, wrong type, extra field, oversized value, invalid enum
AuthMissing token, expired token, wrong scope, wrong tenant, revoked account
UpstreamTimeout, rate limit, empty result, partial record, malformed record
ResultSchema mismatch, missing fallback content, leaked secret, isError omitted
ResourceMissing URI, wrong MIME type, stale bundle, CSP block, unreadable resource
WorkflowStale revision, unknown handle, expired handle, changed field name
MutationDuplicate request, retry after timeout, concurrent write, failed confirmation
CapabilityMethod 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:

  1. Connect the deployed HTTPS MCP endpoint.
  2. Refresh tool metadata and inspect the discovered fields.
  3. Run one direct prompt and one indirect prompt.
  4. Complete one authenticated read and one confirmed write, if the app supports writes.
  5. Verify the UI has no console errors and restores the intended state.
  6. 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

Documentation →
npx sunpeak new

Further Reading

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.