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

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:
| Target | Server location | Transport | What needs a real-client check |
|---|---|---|---|
| Claude custom connector | Public remote server | Streamable HTTP | Connector setup, cloud reachability, OAuth, tool approval, interactive View |
| Claude Code | Local or remote server | stdio or HTTP | Scope precedence, local process config, OAuth callback, allowed tools |
| Claude Messages API MCP connector | Public remote server | HTTP through the API | Beta 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:
| Layer | Main failure it catches | Normal schedule |
|---|---|---|
| Unit tests | Validation, transforms, access rules, idempotency | Every pull request |
| MCP contract tests | Discovery, schemas, annotations, results, resource links | Every pull request |
| Authorization tests | Tenant isolation, scopes, approval, retries | Every pull request |
| Inspector browser tests | View states, bridge calls, host context, responsive UI | Every pull request |
| Production-resource tests | Bundling, assets, CSP, CORS, environment drift | Every pull request or release |
| Model evals | Tool choice, arguments, multi-tool sequence | Metadata changes, main, or nightly |
| Live Claude checks | Real connection, OAuth, host UI, production routing | Main 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:
contentgives Claude a concise, model-readable account of the result.structuredContentcarries typed data that should matchoutputSchema._metacarries app or host metadata that should not become model narration.isErrormarks 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:
- The server rejects a caller without the required scope.
- A user cannot read or change another tenant’s record.
- The View shows the proposed change before a destructive call.
- Cancelling approval makes no change.
- Repeating an idempotent request does not duplicate the effect.
- 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:
- Add or refresh the public MCP endpoint.
- Complete OAuth with a dedicated test account.
- Enable only the tools needed for the test.
- Ask a direct prompt that should call the main read tool.
- Confirm the result and interactive View render.
- Exercise one write approval, cancellation, or error path.
- 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, andresources/readexpose the intended contract. - Every tool has reviewed input schema, descriptions, and safety annotations.
- Successful
structuredContentmatchesoutputSchema. -
content,structuredContent,_meta, andisErrorcarry 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
npx sunpeak newFurther Reading
- Claude Connector framework - build and test interactive connectors
- Claude simulator for MCP Apps
- Claude Connector OAuth authentication
- Claude Connector data access patterns
- MCP App testing strategy
- MCP App outputSchema and structuredContent
- Cross-host testing for MCP Apps
- MCP App CI/CD with GitHub Actions
- Anthropic custom connectors with remote MCP
- Claude Code MCP documentation
- Claude Messages API MCP connector
- Official MCP Apps overview
- MCP Apps client support matrix
- MCP tools specification
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.