Skip to main content
All posts

Pre-Submission Testing for MCP Apps: Validate Before Publishing to ChatGPT and Claude (July 2026)

Abe Wheeler
MCP AppsMCP App TestingMCP App FrameworkChatGPT AppsChatGPT App TestingChatGPT App FrameworkClaude ConnectorsClaude Connector TestingClaude Connector FrameworkPre-Submission TestingPlugin Submission
Testing your MCP App before submitting it as a ChatGPT plugin or to the Claude Connectors Directory.

Testing your MCP App before submitting it as a ChatGPT plugin or to the Claude Connectors Directory.

You built an MCP App. It works in your local inspector. Now you want to submit it to ChatGPT as a plugin or list it in the Claude Connectors Directory so users can install it without a custom setup.

This guide is the pre-submission pass I would run before sending an app to review. It covers protocol checks, UI checks, privacy checks, reviewer credentials, and the current requirements that changed since the first version of this article.

TL;DR: OpenAI now publishes Apps as Plugins, but the app stays an MCP App or Apps SDK app. In the plugin portal, choose With MCP and submit the production MCP server rather than an existing ChatGPT app ID. Every tool needs accurate annotations, names, descriptions, inputs, outputs, and privacy coverage. Prepare exactly five positive and three negative test cases, plus reviewer credentials that work without MFA, confirmation codes, or private-network access. Use sunpeak to run MCP App tests against ChatGPT and Claude host replicas before you submit.

What Changed Since April 2026

Four ecosystem changes matter for pre-submission testing:

  • OpenAI now submits and publishes Apps as Plugins. A plugin can contain an MCP-backed app, skills, or both. An app-only plugin still uses the same MCP App or Apps SDK implementation.
  • The new plugin submission portal requires the production MCP server URL, verified publisher identity, Apps Management write access, domain verification, exact CSP domains, tool scans, three required tool annotations, starter prompts, exactly five positive and three negative test cases, country or region availability, and release notes.
  • Anthropic moved the Connectors Directory guidance from the older help-center FAQ into Claude developer docs. The current submission page says directory submissions include remote MCP servers, desktop extensions, and MCP Apps, with MCP Apps requiring carousel screenshots.
  • The MCP Apps extension is now the better neutral reference for cross-host UI behavior. It defines the app lifecycle, host context, display modes, app-only tools, sandboxed iframes, and declarative CSP in host-agnostic language.

The practical result: reviewers are checking more than “does it load?” They are checking whether the model can choose tools safely, whether users see relevant output, whether the UI works on the surfaces you support, and whether the data you collect matches what you disclose.

Build the Submission Gate First

Before you touch the submission portal, create a release gate that a teammate can run:

pnpm test
pnpm test:e2e
pnpm test:visual
pnpm test:live

Your exact commands may differ, but the gate should cover:

  • Protocol-level tool checks with tools/list and tools/call
  • Rendered UI checks in every supported display mode
  • Light and dark theme screenshots
  • Mobile viewport checks
  • Authentication and reviewer-account checks
  • Privacy and data-minimization checks
  • CSP and external-domain checks
  • Live-host checks for ChatGPT and Claude when possible

sunpeak gives you this shape out of the box. The testing framework runs protocol tests with the mcp fixture, UI tests with the inspector fixture, and host-specific E2E projects for ChatGPT and Claude replicas. That matters because host bugs often hide in UI details, such as iframe height, safe areas, theme tokens, and how a host presents write confirmations.

Tool Annotation Testing

Tool annotations are still the highest-value pre-submission check because they affect model behavior, user trust, and reviewer approval.

For ChatGPT, set these explicitly:

AnnotationSet to true whenExample
readOnlyHintThe tool only fetches, lists, searches, or reads dataget_order_status, search_products
destructiveHintThe tool can create, update, delete, send, revoke, or trigger an irreversible actiondelete_file, send_invoice
openWorldHintThe tool can affect external systems or publicly visible internet statepublish_post, send_email, open_github_issue

For Claude Directory submissions, every tool needs a title and the applicable readOnlyHint or destructiveHint. I still recommend setting all relevant annotations consistently across hosts because the same MCP server often serves multiple clients.

In a sunpeak project, annotations live beside the tool config:

import type { AppToolConfig } from 'sunpeak/mcp';

export const tool: AppToolConfig = {
  resource: 'order-status',
  title: 'Get Order Status',
  description: 'Look up the current status of an order by order ID',
  annotations: {
    readOnlyHint: true,
    destructiveHint: false,
    openWorldHint: false,
  },
};

Then enforce them with integration tests:

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

test('every tool has explicit safety annotations', async ({ mcp }) => {
  const { tools } = await mcp.listTools();

  for (const tool of tools) {
    expect(tool.title, `${tool.name} missing title`).toBeTruthy();
    expect(tool.annotations?.readOnlyHint, `${tool.name} missing readOnlyHint`).toEqual(
      expect.any(Boolean)
    );
    expect(tool.annotations?.destructiveHint, `${tool.name} missing destructiveHint`).toEqual(
      expect.any(Boolean)
    );
    expect(tool.annotations?.openWorldHint, `${tool.name} missing openWorldHint`).toEqual(
      expect.any(Boolean)
    );
  }
});

test('write tools are not marked read-only', async ({ mcp }) => {
  const { tools } = await mcp.listTools();
  const writeVerbs = /^(create|update|delete|send|publish|submit|revoke|run)_/;

  for (const tool of tools.filter((candidate) => writeVerbs.test(candidate.name))) {
    expect(tool.annotations?.readOnlyHint, `${tool.name} changes state`).toBe(false);
    expect(tool.annotations?.destructiveHint, `${tool.name} should be reviewed as a write`).toBe(
      true
    );
  }
});

Do not rely only on naming conventions. Add a small manual review table for tools with side effects because run_report might read data in one app and enqueue a background job in another.

Tool Name, Description, Input, and Output Tests

Reviewers and models use the tool contract as the manual for your app. A tool with a broad description, vague input schema, or noisy output is harder to review and harder for the model to call correctly.

Test these rules:

  • Tool names are unique and action-specific, such as get_invoice or create_ticket
  • Descriptions say what the tool does, not why your app is better
  • Inputs are specific to the task and avoid raw chat transcripts, broad context blobs, and unnecessary location fields
  • Outputs return the data the user needs, without debug fields, logs, trace IDs, or unrelated identifiers
  • Tools that return structured data include structuredContent, and if you define an outputSchema, responses conform to it

The structured output part is easy to miss. The current MCP spec allows tools to return structuredContent and an optional outputSchema. If you use them, validate them before submission:

test('order lookup returns declared structured content', async ({ mcp }) => {
  const result = await mcp.callTool('get_order', { orderId: 'test-123' });

  expect(result.isError).toBeFalsy();
  expect(result.structuredContent).toMatchObject({
    orderId: 'test-123',
    status: expect.any(String),
  });
});

Use MCP App output schema and structuredContent if your UI depends on structured data. It keeps the model-visible response concise while giving the app view the data it needs.

Reviewer Credential Testing

Broken reviewer access is one of the easiest problems to avoid.

Before submission, test your submitted URL and credentials from a clean browser profile outside your company network. Do not assume a logged-in developer session proves anything.

Your reviewer account should have:

  • A dedicated username and password
  • No MFA, SMS, email code, SSO, VPN, or IP allowlist requirement
  • Stable sample data for every submitted test case
  • Permissions that match a normal user, unless your app is explicitly an admin tool
  • Credentials that do not expire during review
  • A clear recovery path if the reviewer locks the account

Add one test that uses the same account shape as the reviewer:

test('reviewer account can call all public tools', async ({ mcp }) => {
  const { tools } = await mcp.listTools();

  for (const tool of tools.filter((candidate) => candidate.visibility?.includes('model') ?? true)) {
    const sampleInput = sampleInputs[tool.name];
    if (!sampleInput) continue;

    const result = await mcp.callTool(tool.name, sampleInput);
    expect(result.isError, `${tool.name} failed for reviewer account`).toBe(false);
  }
});

Store test inputs in fixtures so the reviewer instructions, automated tests, and screenshots all use the same data.

Privacy and Data Minimization Testing

The best privacy review is boring. Every input field should be necessary, every returned field should be expected, and the privacy policy should name every category of user data the app returns or stores.

Audit inputs first:

test('tools do not request broad context fields', async ({ mcp }) => {
  const { tools } = await mcp.listTools();
  const bannedFieldNames = /conversation|transcript|fullContext|rawChat|password|token|apiKey/i;

  for (const tool of tools) {
    const properties = tool.inputSchema?.properties ?? {};
    expect(Object.keys(properties), `${tool.name} asks for broad or secret input`).not.toEqual(
      expect.arrayContaining([expect.stringMatching(bannedFieldNames)])
    );
  }
});

Then audit outputs:

test('tool responses do not leak internal metadata', async ({ mcp }) => {
  const result = await mcp.callTool('get_order', { orderId: 'test-123' });
  const responseText = JSON.stringify(result);

  expect(responseText).not.toMatch(/session[_-]?id/i);
  expect(responseText).not.toMatch(/trace[_-]?id/i);
  expect(responseText).not.toMatch(/request[_-]?id/i);
  expect(responseText).not.toMatch(/internal[_-]?account/i);
  expect(responseText).not.toMatch(/access[_-]?token/i);
});

Do a manual pass too. Automated regex checks will not tell you whether customerSegment is needed for a support-ticket lookup or whether a nested ownerEmail field is covered by your policy.

MCP App Rendering Tests

An MCP App is more than a tool result with HTML attached. The host discovers UI resources, renders the view in a sandboxed iframe, sends host context, passes tool input and result data, and lets the view call app-visible tools when allowed.

That lifecycle creates failure modes that unit tests miss:

  • The resource URI is wrong or missing
  • The iframe initializes after data is sent
  • The view assumes browser storage, cookies, or parent DOM access it does not have
  • The app depends on a host CSS variable without a fallback
  • App-only tools are visible to the model, adding noise to tool selection
  • The view does not handle teardown, refresh, or repeated tool results

Cover those with E2E tests:

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

const displayModes = ['inline', 'pip', 'fullscreen'] as const;

for (const displayMode of displayModes) {
  test(`dashboard renders in ${displayMode}`, async ({ inspector }) => {
    const result = await inspector.renderTool('show-dashboard', undefined, {
      displayMode,
    });

    const app = result.app();
    await expect(app.locator('[data-testid="dashboard"]')).toBeVisible();

    const hasHorizontalOverflow = await app.evaluate(() => {
      return document.documentElement.scrollWidth > document.documentElement.clientWidth;
    });

    expect(hasHorizontalOverflow).toBe(false);
  });
}

If your app only supports one display mode, test only that mode. The mistake is claiming support for modes you never test.

Theme, Mobile, and Screenshot Tests

ChatGPT review asks that test cases pass on web and mobile. Claude Directory submissions for MCP Apps ask for carousel screenshots cropped to the app response. Treat both as testable artifacts.

At minimum:

  • Test light and dark themes
  • Test a 375px mobile viewport
  • Test touch-friendly controls, especially if desktop uses hover
  • Test long labels, empty states, and failed network states
  • Generate fresh screenshots from the same fixtures you list in the submission form
for (const theme of ['light', 'dark'] as const) {
  test(`search app renders in ${theme} theme`, async ({ inspector }) => {
    const result = await inspector.renderTool('search-products', { query: 'desk' }, { theme });
    await expect(result.app().locator('[data-testid="results"]')).toBeVisible();
  });
}

test('search app renders on mobile', async ({ inspector, page }) => {
  await page.setViewportSize({ width: 375, height: 812 });

  const result = await inspector.renderTool('search-products', { query: 'desk' });
  const app = result.app();

  await expect(app.locator('[data-testid="results"]')).toBeVisible();
  await expect(app.locator('button').first()).toHaveCSS('min-height', /4[4-9]px|[5-9]\dpx/);
});

For screenshots, avoid staging data that will disappear. If a reviewer sees a screenshot of one UI state and your test account has different data, you make the review harder.

MCP App views run in sandboxed iframes. They communicate with the host through postMessage, and network access is controlled by declared CSP metadata.

For ChatGPT Apps, use the current _meta.ui.csp shape where possible and keep the older OpenAI compatibility keys only when you need them. Review these fields:

  • connectDomains for API calls
  • resourceDomains for static assets
  • frameDomains only when embedding a third-party experience is essential
  • Redirect or allowed link targets when the host asks for them

OpenAI says apps using frameDomains get extra manual review and are often not approved for broad distribution. If you can replace an embedded third-party page with a native app view, do that before submission.

For Claude, the current Connectors Directory submission flow asks for allowed link URIs when your connector opens external links. List only origins or custom URI schemes you own.

Test CSP by making the app prove its external dependencies work:

test('weather app can reach declared API domain', async ({ inspector }) => {
  const result = await inspector.renderTool('show-weather', { city: 'Seattle' });
  const app = result.app();

  await expect(app.locator('[data-testid="temperature"]')).toBeVisible();
  await expect(app.locator('[data-testid="network-error"]')).toBeHidden();
});

Read MCP App CSP domains before submission if your app loads images, fonts, maps, analytics, or iframe content from external origins.

Cross-Host Testing

ChatGPT and Claude can both render MCP App-style UIs, but they do not have identical host chrome, fonts, iframe sizing, auth flows, display modes, or review forms.

Run the same tests against both host profiles:

pnpm test:e2e

With sunpeak, the test config can create separate Playwright projects for ChatGPT and Claude replicas, so the same test runs once per host. When a test fails, the report shows which host failed.

For submission, also run one live pass:

  • Connect your MCP URL in ChatGPT Developer Mode and run every submitted test case
  • Add the same server as a Claude custom connector or use MCP Inspector for the Claude path
  • Confirm OAuth callback URLs and allowed redirect domains match the real submitted domain
  • Compare generated text output, not only UI rendering

This catches the common problem where the app looks right but the tool’s model-visible result is stale, noisy, or missing the detail the model needs for a follow-up call.

Claude Directory-Specific Checks

Claude’s current submission docs split directory submissions into remote MCP servers, desktop extensions, and MCP Apps. For remote MCP servers and MCP Apps, submission happens in Claude.ai admin settings, and the portal syncs your tools, prompts, and resources from the connected server.

Before submitting a Claude Connector, confirm:

  • The server URL is https://
  • The transport selection in the portal matches your server
  • Tool titles and annotations are present before the portal syncs tools
  • Authenticated services use OAuth 2.0 unless you have coordinated a different mode
  • Documentation and privacy policy URLs are public
  • Test-account instructions include every URL, credential, setup step, and sample prompt the reviewer needs
  • MCP Apps include 3 to 5 PNG carousel screenshots cropped to the app response
  • You ran every tool through MCP Inspector or as a custom connector in Claude

The portal also asks about use cases, company details, data handling, and compliance. Write those answers from the same source of truth as your tests. If your submission says the connector only reads data, your write-tool tests and annotations should agree.

ChatGPT-Specific Checks

For ChatGPT, you now submit a plugin that contains the app. Choose With MCP even when the plugin contains only the app and no skills. Submit the production MCP server URL, not an existing plugin_asdk_app... ID.

Before opening the portal, verify:

  • The submitter has Apps Management: Write access in the OpenAI Platform organization
  • The publisher has completed individual or business identity verification
  • The MCP URL is reachable from a public network
  • The domain can serve the portal’s exact token at /.well-known/openai-apps-challenge
  • The reviewer account works without MFA, SMS, email codes, SSO, or private-network access
  • Every tool has accurate readOnlyHint, openWorldHint, and destructiveHint values
  • Tool responses avoid unrelated user identifiers and internal metadata
  • The privacy policy names the categories of personal data the app collects, returns, stores, and shares
  • The app does not use app or tool descriptions to manipulate model selection
  • frameDomains is absent unless embedding is essential
  • The submission contains exactly five positive and three negative test cases with expected behavior and reproducible fixture or account data
  • Listing details, starter prompts, availability, release notes, support, privacy, and terms URLs are ready

Submitting starts review. After approval, the developer chooses when to publish the plugin. Published app-only, skills-only, and app-plus-skills plugins all appear in the same universal plugin directory in ChatGPT and Codex. There is no separate Apps Directory.

The Pre-Submission Checklist

Run this checklist before submitting to either platform.

Tool contract

  • Every tool has a unique, action-specific name
  • Every tool has a human-readable title
  • Every tool description matches real behavior
  • Every tool has explicit readOnlyHint, destructiveHint, and openWorldHint
  • Write tools are not marked read-only
  • Public or external side effects set openWorldHint: true
  • Inputs are narrow and task-specific
  • Outputs are relevant, concise, and free of debug metadata
  • structuredContent and outputSchema are valid where used

Reviewer access

  • Public MCP URL works outside your network
  • Demo account exists and has stable sample data
  • MFA, SMS, email-code login, VPN, IP allowlists, and SSO are disabled for reviewers
  • Credentials have not expired
  • Reviewer instructions list exact prompts, expected outputs, and any setup steps

Privacy and data

  • Privacy policy is public
  • Privacy policy covers collected, returned, stored, and shared user data
  • Tool inputs do not request raw chat logs, broad context, auth secrets, or unnecessary location data
  • Tool responses do not leak session IDs, trace IDs, request IDs, logs, tokens, or unrelated personal data
  • Restricted data is not collected unless the app is built, disclosed, and reviewed for that use case

UI and host behavior

  • App renders in every declared display mode
  • App works in light and dark themes
  • App works at mobile viewport widths
  • No horizontal overflow appears in supported modes
  • Touch targets are usable on mobile
  • Loading, empty, error, cancelled, and retry states are visible and clear
  • App-only tools are hidden from model-visible tool selection
  • UI survives repeated tool results, refreshes, and teardown

CSP and external access

  • connectDomains includes every API origin the app calls
  • resourceDomains includes required asset origins
  • frameDomains is absent unless embedding is essential
  • Allowed external link targets are listed when the host requires them
  • No staging, localhost, or private-network URLs remain in submitted metadata

Claude Directory

  • Server is reachable over https://
  • Transport selection matches the server
  • OAuth setup matches the auth mode described in the portal
  • Documentation URL and privacy URL are public
  • MCP App screenshots are PNGs, cropped to the app response, and based on stable fixtures
  • Every tool was run through MCP Inspector or as a custom connector in Claude

ChatGPT

  • Submitter has Apps Management write access
  • Publisher identity is verified in the same Platform organization and project
  • MCP domain verification token is available at the generated well-known URL
  • Submission has exactly five positive and three negative test cases
  • Starter prompts, country or region availability, and release notes are complete
  • Website, support, privacy, and terms URLs are public and match the publisher
  • The app does not sell or advertise in ways the policy disallows
  • App and tool descriptions do not compare against or disparage other services
  • UI and model-visible outputs both match the submitted expected behavior

Run the Gate, Then Submit

With sunpeak, your final pre-submission run should look like this:

pnpm test
pnpm test:e2e
pnpm test:visual
pnpm test:live

Then do one manual read of the submission form next to your test report. The goal is consistency: the form, tool annotations, test cases, test account, privacy policy, and rendered app should all describe the same product behavior.

If those agree, you are no longer hoping review catches nothing. You have already tested the same things reviewers are likely to check.

Get Started

Documentation →
npx sunpeak new

Further Reading

Frequently Asked Questions

What should I test before submitting an MCP App to ChatGPT or Claude?

Test tool annotations, tool names and descriptions, test credentials, privacy policy coverage, data minimization, output relevance, error states, CSP domains, display modes, mobile rendering, OAuth, and remote MCP transport. For a ChatGPT plugin that contains an app, prepare exactly five positive and three negative test cases. Run protocol-level tests, E2E tests against the rendered UI, and at least one live-host pass before submission.

Which tool annotations does my MCP App need before submission?

ChatGPT expects readOnlyHint, destructiveHint, and openWorldHint to be set so reviewers can see whether each tool reads data, changes data, or affects external/public systems. Claude Directory submissions require each tool to include a title and the applicable readOnlyHint or destructiveHint. In practice, set all relevant annotations explicitly for every tool and add automated tests that fail when a new tool ships without them.

Why do MCP Apps get rejected during app review?

Common rejection causes include unreachable MCP URLs, broken or expired test credentials, MFA on reviewer accounts, test cases that fail on web or mobile, missing or incorrect tool annotations, privacy policies that do not disclose returned user data, excessive tool inputs, diagnostic data in tool responses, misleading tool descriptions, incomplete apps, and CSP or iframe choices that do not meet platform policy.

How do I test a ChatGPT App before submitting it?

Use Developer Mode or a local host replica to run every tool and UI state. Verify the submitted MCP URL works from outside your network, provide a reviewer demo account with no MFA or confirmation step, run exactly five positive and three negative cases, inspect tool responses for unnecessary user data, and confirm the resource CSP includes every external domain the app needs.

How do I test a Claude Connector before submitting it to the Connectors Directory?

Confirm the server is reachable over HTTPS, exposes the expected remote MCP transport, syncs tools with titles and annotations, uses OAuth 2.0 for authenticated services, has clear documentation and privacy links, includes detailed test-account steps, and has already been exercised with MCP Inspector or as a custom connector in Claude. MCP Apps also need carousel screenshots cropped to the app response.

Do MCP Apps need mobile and display mode testing?

Yes. ChatGPT review checks that test cases pass on web and mobile. MCP Apps also need to behave correctly in the display modes they support, such as inline, fullscreen, and picture-in-picture. Test at small viewport widths, with touch input, in light and dark themes, and with long or empty data.

What privacy checks should I automate for MCP App submission?

Automate checks that tool inputs only request task-specific fields, tool responses do not include session IDs, trace IDs, internal account IDs, logs, auth secrets, raw chat history, or unrelated personal data, and every user-related field returned by a tool is covered by the published privacy policy. For high-risk domains, add manual review before submission.

Can sunpeak test the same MCP App for ChatGPT and Claude?

Yes. sunpeak can run MCP App tests against replicated ChatGPT and Claude runtimes locally and in CI. The testing framework covers protocol-level tool calls, rendered app states, display modes, themes, visual regression, and live-host checks, which helps catch cross-host issues before app review.