Skip to main content
All posts

How to Test Claude Connectors: Unit Tests, Local Inspector, and CI/CD (September 2026)

Abe Wheeler
Claude ConnectorsClaude Connector TestingClaude Connector FrameworkMCP AppsMCP App TestingClaude AppsMCP Testing Framework
Testing Claude Connectors locally with the sunpeak inspector.

Testing Claude Connectors locally with the sunpeak inspector.

TL;DR: Test a Claude Connector at each boundary it crosses. Start with MCP discovery and tool-result contracts, then pin important UI states and render them in a local Claude runtime. Test OAuth, authorization, the production bundle, and public reachability separately. Keep real Claude checks small because they cost more and change outside your repository.


A Claude Connector can pass a handler unit test and still fail for users. Claude must reach the remote MCP endpoint, discover the intended tool, send valid arguments, receive a useful result, and apply the right approval flow. An interactive connector adds a ui:// resource, sandboxed iframe, app bridge, host capabilities, and browser security rules.

That is why testing only “does Claude call my tool?” misses most of the system.

Anthropic’s current custom connector documentation says remote connectors are available across Claude, Cowork, Claude Desktop, and mobile clients. Those connections originate from Anthropic’s cloud infrastructure, even when the user runs Claude Desktop. Interactive connectors can appear as inline cards or fullscreen Views.

The shared UI contract is now MCP Apps, the first official MCP extension. Claude is one supported host, but extension support still varies by client and capability. Your test plan should therefore separate portable MCP behavior from Claude-specific setup and real-host behavior.

Define the Product Surface First

“Claude Connector” can refer to more than one client path. Write down which paths you support before you build the test matrix:

TargetServer locationTransportWhat needs a real-client check
Claude custom connectorPublic remote serverStreamable HTTPConnector setup, cloud reachability, OAuth, tool approval, interactive View
Claude CodeLocal or remote serverstdio or HTTPScope precedence, local process config, OAuth callback, allowed tools
Claude Messages API MCP connectorPublic remote serverHTTP through the APIBeta header, server configuration, allowlists, tool use in API responses

Claude Code recommends HTTP for remote servers and marks its older SSE transport as deprecated. The Messages API MCP connector is a separate beta feature. Do not treat one successful claude.ai conversation as proof that every Claude client path works.

For an interactive connector, also name the MCP Apps capabilities you depend on: tool calls from the View, model-context updates, external links, display-mode requests, browser permissions, or host context. Test each required capability and its fallback because hosts can implement optional features on different schedules.

Use a Boundary-Based Test Plan

A practical Claude Connector suite has seven layers:

LayerMain failure it catchesNormal schedule
Unit testsValidation, transforms, access rules, idempotencyEvery pull request
MCP contract testsDiscovery, schemas, annotations, results, resource linksEvery pull request
Authorization testsTenant isolation, scopes, approval, retriesEvery pull request
Inspector browser testsView states, bridge calls, host context, responsive UIEvery pull request
Production-resource testsBundling, assets, CSP, CORS, environment driftEvery pull request or release
Model evalsTool choice, arguments, multi-tool sequenceMetadata changes, main, or nightly
Live Claude checksReal connection, OAuth, host UI, production routingMain or release candidate

This is closer to a testing map than a strict pyramid. MCP integration and iframe tests often catch more risk than isolated component tests because the product crosses protocol and browser boundaries.

1. Unit Test Tool Logic

Keep tool handlers thin enough to test without an MCP transport. Cover your rules, not the SDK’s serialization.

import { describe, expect, it, vi } from 'vitest';
import { searchTickets } from '../../src/services/tickets';
import handler from '../../src/tools/search-tickets';

vi.mock('../../src/services/tickets');

describe('search-tickets', () => {
  it('returns a typed empty result', async () => {
    vi.mocked(searchTickets).mockResolvedValueOnce([]);

    const result = await handler({ query: 'missing' }, {} as any);

    expect(result.isError).toBeFalsy();
    expect(result.structuredContent).toEqual({ tickets: [], nextCursor: null });
    expect(result.content).toEqual([
      { type: 'text', text: 'No matching tickets.' },
    ]);
  });
});

Unit tests should cover:

  • Input normalization and domain validation.
  • Access checks before any data leaves the server.
  • Empty, partial, rate-limited, timeout, and upstream-error paths.
  • Pagination and stable cursor behavior.
  • Token refresh and revoked credentials.
  • Idempotency and duplicate-call handling for write tools.
  • Redaction of secrets and data the model does not need.

For multi-tenant connectors, test a user who can guess another tenant’s record ID. A correct tool annotation or host confirmation does not replace server-side authorization.

2. Contract Test Discovery and Results

Claude learns what your connector can do from the MCP surface. Test tools/list, resources/list, resources/read, and tools/call against the running server.

The current MCP tools specification defines inputSchema, optional outputSchema, tool annotations, and the content, structuredContent, _meta, and isError result fields. It also says annotations are untrusted hints. Your server still owns access control and side-effect policy.

With sunpeak’s MCP fixture:

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

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

  expect(search).toBeDefined();
  expect(search?.inputSchema.properties).toHaveProperty('query');
  expect(search?.outputSchema?.properties).toHaveProperty('tickets');
  expect(search?.annotations).toMatchObject({
    readOnlyHint: true,
    destructiveHint: false,
  });
  expect(search?._meta?.ui?.resourceUri).toBe('ui://tickets/search');

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

Then call the tool through MCP rather than importing its handler:

test('search result matches the public contract', async ({ mcp }) => {
  const result = await mcp.callTool('search-tickets', {
    query: 'login',
    status: 'open',
  });

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

For each tool, call at least one normal input, one boundary input, and one invalid input. Add unauthorized, expired-token, upstream-error, and write-retry cases when they exist.

Keep Result Fields Separate

A connector result has several data lanes:

  • content gives Claude a concise, model-readable account of the result.
  • structuredContent carries typed data that should match outputSchema.
  • _meta carries app or host metadata that should not become model narration.
  • isError marks a tool execution failure the model may be able to recover from.

Test each lane directly. If a View needs 500 records but Claude only needs totals and source links, do not mirror the full UI payload into model-visible text. Also test your compatibility target: the 2025-11-25 tools spec recommends serialized JSON in a text content block when returning structuredContent for older clients.

3. Test Safety and Authorization as Behavior

Create a policy table instead of making one generic annotation assertion:

const expectedSafety = {
  'search-tickets': {
    readOnlyHint: true,
    destructiveHint: false,
    idempotentHint: true,
    openWorldHint: false,
  },
  'create-ticket': {
    readOnlyHint: false,
    destructiveHint: false,
    idempotentHint: false,
    openWorldHint: false,
  },
  'delete-ticket': {
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: true,
    openWorldHint: false,
  },
} as const;

Fail when tool metadata drifts from that reviewed policy. Then test actual behavior:

  1. The server rejects a caller without the required scope.
  2. A user cannot read or change another tenant’s record.
  3. The View shows the proposed change before a destructive call.
  4. Cancelling approval makes no change.
  5. Repeating an idempotent request does not duplicate the effect.
  6. Partial failure returns an accurate state and recovery path.

Anthropic lets users disable tools and asks for approval around tool use. Treat those controls as another safety layer, not your authorization system.

4. Test OAuth in Three Places

OAuth failures happen at different boundaries, so split the suite.

Application Tests

Mock the credential state and prove your own code handles:

  • No token.
  • Expired access token with a successful refresh.
  • Failed refresh or revoked grant.
  • Missing required scope.
  • Correct token attached to the correct upstream origin.
  • Disconnect, reconnect, and account switching.

Protocol Tests

From an unauthenticated MCP client, inspect the challenge and discovery path. Check protected resource metadata, authorization server metadata, advertised scopes, PKCE requirements, redirect URI handling, and token audience validation. Use the MCP authorization specification as the contract.

Live Tests

Claude remote connector traffic starts in Anthropic’s cloud. A local request or VPN-connected browser does not prove Claude can reach the server. In staging, test the public HTTPS endpoint, current Anthropic network allowlist rules, OAuth callback, consent, token refresh, disconnect, and reconnect.

Do not put production credentials in simulations, screenshots, traces, or CI artifacts. Use a dedicated test tenant and the smallest scopes that cover the test.

5. Pin Interactive States with Simulations

An interactive connector is an MCP App: a tool points at a ui:// resource, the host fetches HTML, and a sandboxed iframe communicates with the host over the app bridge. The stable MCP Apps specification gives you the portable contract. The client matrix shows why capability fallbacks still need tests.

Use deterministic simulation files for UI states that are slow or risky to create against a live backend:

{
  "tool": "search-tickets",
  "userMessage": "Show open login tickets",
  "toolInput": {
    "query": "login",
    "status": "open"
  },
  "toolResult": {
    "content": [
      { "type": "text", "text": "Found two open tickets." }
    ],
    "structuredContent": {
      "tickets": [
        { "id": "TICK-1", "title": "Login error", "status": "open" },
        { "id": "TICK-2", "title": "SSO timeout", "status": "open" }
      ],
      "nextCursor": null
    }
  }
}

Store simulations in tests/simulations/*.json. Use separate files for normal, empty, partial, error, auth-required, long-text, large-data, and write-confirmation states. Mock View-initiated server tool calls in the simulation too, so the same fixture covers the complete interaction.

Avoid a full combination of every state, theme, mode, and viewport. Pick cases that cover each risk and add a combination when the dimensions can interact, such as dense data in fullscreen or an error banner at mobile width.

6. Render the View in a Claude Runtime

For a sunpeak project, run:

pnpm dev

For an existing MCP server in any language:

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

The sunpeak Inspector runs Claude and ChatGPT-style host replicas locally. It lets you change tool data, host, theme, display mode, and viewport without deploying or spending host credits.

Turn the important states into Playwright tests:

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

test('open tickets are usable in dark mode', async ({ inspector }) => {
  const result = await inspector.renderTool(
    'search-tickets',
    { query: 'login', status: 'open' },
    { theme: 'dark', displayMode: 'inline' }
  );

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

  await expect(app.getByRole('heading', { name: /open tickets/i })).toBeVisible();
  await app.getByRole('button', { name: /open TICK-1/i }).click();
  await expect(app.getByText(/Login error/)).toBeVisible();
});

sunpeak selects the host through the Playwright project. Do not pass a host option to renderTool(). Run the same semantic test in each configured host project, and branch only when a documented capability requires different behavior.

Your browser suite should check:

  • Visible content, accessible names, keyboard order, and focus recovery.
  • Narrow and wide layouts with no clipping or horizontal overflow.
  • Light and dark themes.
  • Inline and fullscreen behavior where supported.
  • Capability-present and capability-missing fallbacks.
  • View-initiated server tool calls, errors, retries, and cancellations.
  • Console errors, failed network requests, CSP violations, and broken assets.
  • App state or model context that must survive the next user action.

Add visual baselines for dense, responsive, or branded states, but keep semantic assertions. A screenshot can look plausible while a button has no accessible name or a tool call fails.

7. Test the Production Resource

Development mode can hide bundle failures. HMR injects scripts, source assets may resolve differently, and localhost often has looser origin assumptions.

Build the connector, then rerun the main browser path against production resources:

test('production resource loads in Claude dark mode', async ({ inspector }) => {
  const result = await inspector.renderTool(
    'search-tickets',
    { query: 'login' },
    { theme: 'dark', prodResources: true }
  );

  expect(result).not.toBeError();
  await expect(result.app().getByText('Login error')).toBeVisible();
});

Verify bundled HTML, JavaScript, CSS, fonts, images, and source maps. Check that resource CSP metadata lists every production connection and asset origin, with no development origins. Exercise a cold load, remount, repeated tool call, and expired session.

8. Add Model Evals for Tool Choice

Contract tests can prove a tool description exists. They cannot prove Claude will choose the right tool from a crowded catalog.

Add evals when tools overlap, arguments are often confused, follow-up prompts depend on app context, or tool sequencing matters. Include direct requests, natural paraphrases, negative prompts, missing arguments, read versus write intent, multi-step requests, and follow-ups that depend on View state.

Measure repeated pass rates for tool choice, arguments, order, and final result. Record the model, tool catalog, prompt set, latency, and failure reason so a description change can be compared with the previous version.

sunpeak supports multi-model evals separately from deterministic tests. Keep them out of the fast pull-request gate unless their cost and variance are acceptable.

9. Keep Live Claude Tests Narrow

A local replica cannot prove that the current Claude product accepts your deployed connector. A useful live smoke plan covers:

  1. Add or refresh the public MCP endpoint.
  2. Complete OAuth with a dedicated test account.
  3. Enable only the tools needed for the test.
  4. Ask a direct prompt that should call the main read tool.
  5. Confirm the result and interactive View render.
  6. Exercise one write approval, cancellation, or error path.
  7. Disconnect, reconnect, and repeat the primary read.

At the time of this update, sunpeak’s deterministic Inspector includes a Claude runtime replica, while its packaged live browser adapter targets ChatGPT. Run real Claude checks manually or in a small host-specific harness, and do not imply that a local replica is the production Claude UI.

Keep live checks separate from broad state coverage. They depend on external accounts, product UI, model behavior, network access, and usage limits, so failures need different triage from deterministic CI failures.

GitHub Actions Example

Run local tests on every pull request:

name: Test Claude Connector

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - run: pnpm install --frozen-lockfile
      - run: pnpm exec playwright install --with-deps chromium
      - run: pnpm validate
      - run: pnpm build

Use a separate trusted workflow for model evals, staging OAuth, and real Claude checks. Limit its secrets to a test tenant, do not upload authenticated traces, and make external failures visible without blocking unrelated local development.

Pre-Release Checklist

  • tools/list, resources/list, and resources/read expose the intended contract.
  • Every tool has reviewed input schema, descriptions, and safety annotations.
  • Successful structuredContent matches outputSchema.
  • content, structuredContent, _meta, and isError carry the right data.
  • Authorization, tenant isolation, scopes, retries, and idempotency are tested on the server.
  • OAuth discovery, refresh, revocation, disconnect, and reconnect work.
  • Simulations cover normal, empty, partial, error, auth, and write states.
  • Playwright tests render the main workflows in each claimed host runtime.
  • Responsive, theme, display-mode, keyboard, and capability fallbacks are covered.
  • The production resource passes asset, CSP, CORS, and cold-load checks.
  • Model evals cover ambiguous tools and negative prompts where needed.
  • The public HTTPS endpoint is reachable from Anthropic infrastructure.
  • A small real Claude smoke test passes before release.

Where sunpeak Fits

You can assemble this stack with an MCP SDK, a browser runner, schema validators, fixtures, and your own host harness. sunpeak’s Claude Connector framework packages the repeated work: project structure, local server wiring, deterministic simulations, MCP fixtures, Claude and ChatGPT runtime replicas, Playwright E2E tests, visual tests, production-resource checks, and model evals.

Start a new project with npx sunpeak new. For an existing TypeScript, Python, Go, or Rust MCP server, use npx sunpeak test init --server URL and keep the server implementation you already have.

The goal is to make your own contracts, UI states, permissions, and production claims repeatable before the connector reaches a real account.

Get Started

Documentation →
npx sunpeak new

Further Reading

Frequently Asked Questions

How do I test a Claude Connector without a Claude account?

Test the MCP server directly, then render its interactive resources in a local Claude runtime replica. With sunpeak, run npx sunpeak test init --server URL for an existing server or pnpm dev in a sunpeak project. Contract, simulation, E2E, visual, and production-resource tests run without a Claude account or model credits.

What should I test first in a Claude Connector?

Start with discovery and tool-result contracts. Verify tools/list, resources/list, resources/read, tool-to-resource links, input schemas, annotations, and output schemas. Call each tool with valid, empty, invalid, unauthorized, and upstream-error inputs before adding browser tests.

How do I test structuredContent in a Claude Connector?

Call the tool through an MCP client and validate structuredContent against its outputSchema. Also check content, isError, and _meta separately because they have different audiences. Then render the result in the linked UI resource to prove that the server contract and View agree.

How do I test an interactive Claude Connector?

Create deterministic simulations for success, empty, error, auth, long-data, write-confirmation, theme, display-mode, and viewport states. Render each important state in a Claude runtime replica and use Playwright to test the iframe UI, app-initiated tool calls, keyboard flow, console errors, and failed requests.

How do I test Claude Connector OAuth?

Unit test authorization and tenant checks, then protocol test unauthenticated responses and OAuth discovery metadata. Use fixtures for expired, missing-scope, denied, refreshed, and revoked tokens. Finish with a narrow live test against a public HTTPS endpoint because Claude remote connector traffic originates from Anthropic infrastructure.

How do I run Claude Connector tests in GitHub Actions?

Run static checks, unit tests, MCP contract tests, inspector E2E tests, and the production build on every pull request. Install Playwright Chromium on the runner. Keep real Claude checks and model evals in separate trusted jobs because they need accounts, secrets, network access, and usage.

Do Claude Connectors need tests in the real Claude app?

Yes, but keep them small. A host replica cannot prove that Claude accepts the deployed endpoint, completes the real OAuth flow, chooses the intended tool, or renders the current production host UI. Use live checks for those boundaries and keep broad state coverage deterministic and local.

What is different when testing a Claude Connector for Claude Code or the Messages API?

Test the client you plan to support. Claude Code can use local stdio, remote HTTP, project-scoped configuration, and its own OAuth callback flow. The Messages API MCP connector is a separate beta client with tool allowlists and per-tool configuration. A successful claude.ai connector test does not prove either path.