Skip to main content
All posts

Debugging Claude Connectors: Fix Common Errors in Development and Production (August 2026)

Abe Wheeler
Claude ConnectorsClaude Connector TestingClaude Connector FrameworkClaude AppsMCP AppsMCP App FrameworkMCP App TestingChatGPT Apps
Debug Claude Connectors locally with the sunpeak inspector before connecting to Claude.

Debug Claude Connectors locally with the sunpeak inspector before connecting to Claude.

TL;DR: Debug a Claude Connector one boundary at a time: public HTTP, MCP protocol, OAuth, tool discovery, tool choice, tool execution, and MCP App rendering. Capture Claude’s ofid_ reference ID and a request ID, reproduce the tool locally in the sunpeak MCP App Inspector, then turn the failing input and result into a simulation and regression test. This August 2026 guide covers the current Claude and MCP Apps failure modes.

A Claude Connector can fail in several places while showing one broad error. “Couldn’t reach the MCP server” can mean private DNS, a WAF rule, an OAuth discovery failure, or a cross-host redirect. A blank app can mean the tool worked but its ui:// resource never connected, received the wrong result, or rendered at zero height.

Treat the connector as a chain of observable boundaries:

  1. Claude reaches the public MCP URL.
  2. Client and server agree on an MCP protocol revision and transport behavior.
  3. OAuth discovery and token exchange succeed when authentication is required.
  4. tools/list exposes the expected catalog for this user and scope.
  5. Claude selects the intended tool and sends valid arguments.
  6. The handler returns a valid tool result.
  7. The host fetches, starts, and sizes the MCP App resource.
  8. The app handles the full tool lifecycle without throwing.

The first failed boundary is usually the root cause. Fixing later symptoms before proving earlier boundaries wastes time and often hides the bug.

Capture Evidence Before Retrying

Preserve one failed attempt before a reconnect, redeploy, or browser refresh changes it.

Record:

  • The exact user prompt, connector URL, Claude surface, and time.
  • The ofid_ reference ID shown in Claude’s error toast or connector settings URL.
  • The tool name, arguments, result, and negotiated MCP protocol version when available.
  • A request or trace ID shared by edge logs, MCP logs, upstream calls, and the tool result.
  • The inner iframe’s console errors, network failures, and dimensions for MCP App bugs.

Anthropic says ofid_ values are time-limited. Include one with timestamps and server logs in a support report so Anthropic can trace the same connector setup flow. Do not put access tokens, authorization codes, cookies, raw private records, or full tool payloads in logs.

A small correlation field makes production reports much easier to follow:

const requestId = crypto.randomUUID();

logger.info({ requestId, tool: 'search_tickets' }, 'tool started');

return {
  content: [{ type: 'text', text: `Found ${tickets.length} tickets. Reference: ${requestId}` }],
  structuredContent: { tickets, requestId },
};

Keep the user-facing reference opaque. Store sensitive context only in access-controlled server logs, with a short retention period.

Reproduce the Smallest Failing Boundary

For an existing Streamable HTTP server, open it in sunpeak:

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

sunpeak can inspect a stdio command too:

npx sunpeak inspect --server "python server.py"

For a sunpeak project, run:

pnpm dev

Open http://localhost:3000, select the Claude host replica, and load the failing tool or simulation. The inspector exposes tool input, tool result, resources, theme, display mode, host context, and iframe behavior without requiring a paid host account or consuming host credits.

Add a test harness to an existing MCP server with:

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

Then separate server and UI checks:

test('the server returns an actionable permission error', async ({ mcp }) => {
  const result = await mcp.callTool('search_tickets', {
    query: 'refund',
    workspaceId: 'ws_forbidden',
  });

  expect(result.isError).toBe(true);
  expect(result.content[0]).toMatchObject({
    type: 'text',
    text: expect.stringContaining('permission'),
  });
});

test('the Claude view renders the same error state', async ({ inspector }) => {
  const result = await inspector.renderTool('search-tickets-permission-denied');
  const app = result.app();
  await expect(app.getByRole('alert')).toContainText('permission');
});

The first test proves the MCP result. The second proves the host and app can render it. A failure in one no longer looks like a failure in both.

Diagnose Connection Failures

Symptom: Claude reports that it could not reach the MCP server, the connector stays disconnected, or no tool catalog appears.

Start outside your own network:

dig +short connector.example.com
curl -i https://connector.example.com/mcp
curl -sI https://connector.example.com/mcp

A 401, 405, or JSON-RPC response proves the endpoint answered. A timeout, private address, refused connection, 502, edge-generated 403 or 429, or cross-host redirect identifies the next system to inspect.

Check Public DNS and Edge Rules

Claude’s hosted connector service reaches remote MCP servers from Anthropic’s infrastructure, including when the user runs Claude Desktop. Every resolved address must be globally routable. A server behind a VPN, private load balancer, or split DNS zone will not work as a hosted connector.

If application logs show nothing, check DNS, CDN, WAF, bot protection, and rate-limit logs. Anthropic publishes an egress range for allowlisting. Apply the narrowest rule you can to the MCP and OAuth routes rather than weakening the whole site.

Remove Cross-Host Redirects

Register the final MCP URL, including its path and canonical host:

https://connector.example.com/mcp

Do not register an apex URL that redirects to www, a vanity host that redirects to a regional host, or an app route that eventually redirects to /mcp. Claude may follow a cross-host redirect without forwarding the Authorization header, so the target sees an anonymous request and the UI reports an auth failure.

Verify the Transport

Claude currently supports Streamable HTTP and is deprecating legacy HTTP+SSE for connectors. A stdio-only server cannot be registered as a remote URL.

Do not hard-code one protocol revision into middleware. MCP’s July 2026 revision introduced a newer negotiation path and stateless request model, while clients and servers still need to interoperate with 2025-era revisions. Log the negotiated revision, test the revisions your SDK advertises, and let the SDK encode version-specific behavior.

For 2025-era Streamable HTTP traffic, proxies may see Mcp-Session-Id. In the 2026-07-28 revision, HTTP is stateless and that session header is no longer part of the protocol. A gateway that requires the header will reject valid modern requests, while middleware that strips it may break older negotiated sessions.

Check these edge behaviors:

  • POST /mcp reaches the handler without a trailing-slash redirect.
  • Accept: application/json, text/event-stream is preserved when required by the negotiated transport.
  • MCP-Protocol-Version is forwarded and not rewritten.
  • Streaming responses are not buffered into a gateway timeout.
  • A server instance does not keep user state in memory between modern HTTP requests.

Diagnose OAuth and Permission Failures

Symptom: connector setup cannot start OAuth, the callback fails, reconnect loops, or a working user receives 401 or 403 from a tool.

Separate discovery, authorization, token exchange, token validation, and scope enforcement. They produce similar UI messages but need different fixes.

Start at the MCP Challenge

An unauthenticated request should return a 401 with a WWW-Authenticate challenge that points to protected resource metadata, or the metadata must be discoverable at the standard RFC 9728 path. If the MCP endpoint includes /mcp, test the path-specific location too:

curl -i https://connector.example.com/mcp
curl -i https://connector.example.com/.well-known/oauth-protected-resource/mcp
curl -i https://auth.example.com/.well-known/oauth-authorization-server
curl -i https://auth.example.com/.well-known/openid-configuration

Only one authorization server metadata format is required. Its metadata must expose a supported way to identify the OAuth client: Dynamic Client Registration, Client ID Metadata Documents, or credentials you pre-registered for Claude.

Hosted Claude surfaces use this callback:

https://claude.ai/api/mcp/auth_callback

Claude Code uses a loopback redirect, so one callback entry does not cover every client.

Validate the Token for This Resource

Claude sends the OAuth resource parameter using the canonical MCP URL, including its path. Your authorization server should issue a token for that resource, and your MCP server should reject a token minted for another API.

Check:

  • The metadata issuer matches the issuer that signs the token.
  • PKCE S256 is advertised and accepted.
  • The token audience matches the canonical MCP resource.
  • A 403 includes enough scope information for the client to request added access.
  • Refresh failures return standard OAuth errors and lead to a clean reconnect.
  • User, organization, and tenant authorization runs inside every tool handler.

Do not use the OAuth client ID as the end-user ID. The client identifies Claude’s OAuth client, while token subject and tenant claims identify the user and account whose data the tool may access.

Diagnose Missing or Wrong Tools

Symptom: the connector connects, but Claude cannot see a tool, calls the wrong one, or sends arguments that do not fit the schema.

First bypass model choice and inspect discovery:

test('the scoped catalog exposes search_tickets', async ({ mcp }) => {
  const tools = await mcp.listTools();
  expect(tools.map((tool) => tool.name)).toContain('search_tickets');
});

If the tool is missing, check auth scope, feature flags, tenant policy, deployment version, and catalog filtering. Keep catalog order deterministic for the same user and scope so caches, snapshots, and debugging output remain useful.

If the tool is present but not selected, inspect its contract:

export const tool: AppToolConfig = {
  resource: 'ticket-list',
  title: 'Search Tickets',
  description:
    'Use when the user wants to find support tickets by keyword, status, priority, or assignee. Returns ticket ID, title, status, priority, assignee, and updated time.',
};

export const schema = {
  query: z.string().min(1).describe('Words from the ticket title or body'),
  status: z.enum(['open', 'pending', 'closed']).optional(),
};

export const outputSchema = {
  tickets: z.array(ticketSummarySchema),
  nextCursor: z.string().optional(),
};

Claude’s tool guidance recommends differentiating similar tools by when to use them, not only what each tool does. Avoid duplicate names, broad catch-all tools, hidden required inputs, and descriptions that promise data the result does not return.

Test tool choice separately from execution. Keep prompt cases that assert the expected tool, no-tool prompts that should stay in conversation, and ambiguous prompts where either of two tools is acceptable. A direct prompt passing once is not enough evidence for reliable selection.

Diagnose Tool Execution Errors

Symptom: Claude selects the right tool, but the call fails, repeats, or returns a generic server error.

MCP has two error channels:

  • JSON-RPC protocol errors cover malformed requests, unknown methods, and protocol-level server failures.
  • Tool execution errors use a normal tool result with isError: true for invalid business input, expired access, permission denial, rate limits, and upstream failures.

Return a correction Claude can act on:

return {
  isError: true,
  content: [
    {
      type: 'text',
      text: 'The start date must be before the end date. Use ISO 8601 dates.',
    },
  ],
};

Do not expose a stack trace or collapse every exception into “internal server error.” Log the stack with the request ID, then return a short message that says whether the user should change input, reconnect, request access, retry later, or contact support with that reference.

Add timeouts to each outbound call rather than relying on Claude’s outer timeout. Anthropic documents a 300-second timeout for Claude.ai and Claude Desktop, but most tools should fail or queue work within seconds:

const response = await fetch(apiUrl, {
  signal: AbortSignal.timeout(30_000),
});

For work that can take minutes, use explicit start_report and get_report_status tools. Return a durable job ID. Do not keep progress only in an MCP HTTP session because the modern protocol revision does not provide HTTP session state.

Diagnose Blank or Broken MCP Apps

Symptom: the tool succeeds, but Claude shows plain text, an invisible region, a blank card, stale data, or a frontend error.

An MCP App tool advertises a ui:// resource through _meta.ui.resourceUri. The host fetches the HTML resource, places it in a sandboxed iframe, and starts a separate app-to-host JSON-RPC connection over postMessage. Tool success does not prove any of those UI steps worked.

In Claude Desktop, enable Developer Mode under Help > Troubleshooting, open Developer Tools, inspect the tool call, and select the inner iframe. Check the Console, Network panel, computed height, and the order of app lifecycle messages.

Prove Initialization and Size

With the official MCP Apps client, handlers must be installed before connection and the app must connect:

const app = new App({ name: 'ticket-list', version: '1.0.0' });

app.ontoolresult = (result) => renderTickets(result.structuredContent);
app.ontoolcancelled = () => renderCancelled();

await app.connect();

If app.connect() never runs, tool lifecycle handlers do not fire. If the root has no content or the app reports a height of zero, Claude can reserve an invisible iframe even though initialization succeeded.

Prove the Resource Contract

Check that:

  • The tool’s _meta.ui.resourceUri matches a resource returned by resources/read.
  • The resource returns bundled HTML with the MCP App MIME type.
  • structuredContent matches the tool’s outputSchema and the UI’s runtime checks.
  • Private fields needed only by the app stay out of model-visible content.
  • External origins are listed in the resource CSP metadata.
  • App-initiated tool calls are supported by the host and tested with the same arguments.

Test the lifecycle states independently: partial input, complete input, loading, success, empty, execution error, cancellation, malformed output, and teardown. The current MCP Apps SDK also lets an app send structured log messages to a host that advertises logging support, which is useful for initialization and state-transition diagnostics.

Keep Initial Results Small

Anthropic documents an approximate 150,000-character threshold for Claude.ai and Claude Desktop. When code execution is active, a larger tool result may be written to the sandbox filesystem and replaced with a file pointer. The app then does not receive the inline structuredContent it expected, so it may never hydrate.

Return a summary and first page, then use an app-initiated tool call for detail. Claude Code has a separate 25,000-token default controlled by MAX_MCP_OUTPUT_TOKENS, so test payload boundaries per host rather than treating one limit as portable.

Save the Failure as a Simulation

Once you have the input and sanitized result, create a deterministic state under tests/simulations/:

{
  "tool": "search_tickets",
  "userMessage": "Search the support tickets for refund issues",
  "toolInput": {
    "query": "refund",
    "workspaceId": "ws_forbidden"
  },
  "toolResult": {
    "isError": true,
    "content": [
      {
        "type": "text",
        "text": "You do not have permission to search this workspace."
      }
    ]
  }
}

Use serverTools in the same simulation when the app calls another tool after rendering, such as retry, load details, or request access. The UI test can then cover the whole interaction without a live account or unstable backend.

Build a small failure matrix:

BoundaryDirect checkRegression test
Public HTTPDNS, curl, edge logsDeployment smoke test
Protocoldiscovery, negotiated revision, raw JSON-RPCmcp.listTools() and mcp.callTool()
OAuthmetadata, PKCE, issuer, audience, scopesAuth integration tests
Tool choicedirect and ambiguous promptsMulti-model eval cases
Tool handlerinput, result, upstream traceUnit and protocol tests
MCP Appiframe console, lifecycle, CSP, sizeinspector.renderTool() E2E tests
Host behaviorone narrow real Claude reproSmall live test suite

Run the broad state matrix locally across Claude and ChatGPT host replicas, themes, display modes, and widths. Reserve live Claude tests for hosted networking, OAuth redirects, real model selection, and host behavior that a local runtime cannot prove.

A Repeatable Debugging Order

Use this order during an incident:

  1. Save the ofid_, timestamp, prompt, URL, and request ID.
  2. Check public DNS, reachability, redirects, WAF responses, and MCP path.
  3. Inspect protocol discovery, the negotiated revision, and the scoped tool catalog.
  4. Verify OAuth discovery, client registration, PKCE, issuer, audience, and scope.
  5. Call the exact tool directly with the failing arguments.
  6. Classify the failure as a protocol error or tool execution error.
  7. Render the exact result in the Claude host replica and inspect the inner iframe.
  8. Save the state as a simulation and add the smallest regression test that catches it.
  9. Recheck one narrow flow in real Claude when the fix depends on hosted behavior.

The sunpeak Inspector keeps the inner loop local, and the sunpeak testing framework promotes the same server call and UI state into CI. That gives each connector bug a traceable boundary, a reproducible fixture, and a test that prevents the same failure from returning.

Get Started

Documentation →
npx sunpeak new

Further Reading

Frequently Asked Questions

Why is my Claude Connector not working?

First identify the failing layer: public connection, MCP protocol negotiation, OAuth, tool discovery, tool selection, tool execution, or MCP App rendering. Check the Claude ofid_ reference ID and server access logs, call the public MCP URL with curl, inspect the server with an MCP client, call the failing tool directly, then render its UI with the same result. This order prevents a frontend symptom from sending you into backend debugging.

How do I debug a Claude Connector locally?

Run npx sunpeak inspect --server http://localhost:8000/mcp for an existing server, or pnpm dev in a sunpeak project. The inspector lets you switch to the Claude host replica, call tools, inspect tool input and output, render UI resources, and load deterministic simulations. Use npx sunpeak test init --server URL to add protocol and Playwright tests around an existing server.

Why does Claude not call my connector tool?

Claude chooses among enabled tools using their names, descriptions, input schemas, and the conversation. Check that tools/list exposes the tool for the current user, remove name collisions, state when the tool should be used, keep required inputs explicit, and make similar tools differ by intent. Test direct prompts across several models and record the expected tool and arguments as eval cases.

Why is my Claude Connector MCP App blank or invisible?

A blank MCP App often means the app never called app.connect(), the iframe has zero height, the UI resource URI is wrong, the tool result does not contain the structuredContent the app expects, or the resource threw during initialization. Inspect the inner iframe in Claude Desktop Developer Tools and test loading, empty, error, cancelled, malformed, and large-result states locally.

How do I fix Claude Connector OAuth errors?

Confirm the endpoint returns a 401 with a valid WWW-Authenticate resource_metadata pointer or exposes RFC 9728 metadata at the right path. Then verify authorization server discovery, a supported client registration method, PKCE S256, issuer, canonical resource audience, scopes, and refresh behavior. Hosted Claude surfaces use https://claude.ai/api/mcp/auth_callback, while Claude Code uses a loopback redirect.

What do the Claude Connector ofid_ reference IDs mean?

An ofid_ value identifies a failed connector setup flow in Claude. Copy it from the error toast or connector settings URL as soon as the error occurs because it is time-limited. Include the reference ID, MCP server URL, timestamps, and matching edge and application logs when reporting the issue to Anthropic.

What output limits apply to Claude Connector tools?

Anthropic documents an approximate 150,000-character threshold for Claude.ai and Claude Desktop tool results. Above it, Claude may store the result as a file, so an MCP App may receive a pointer instead of the structuredContent it needs. Claude Code uses a separate 25,000-token default controlled by MAX_MCP_OUTPUT_TOKENS. Paginate and fetch details on demand instead of approaching either limit.

How should a Claude Connector report tool errors?

Return expected execution failures, such as invalid filters, expired credentials, permission denial, or upstream API errors, as a tool result with isError: true and a short actionable message. Reserve JSON-RPC protocol errors for malformed requests, unknown methods, and server protocol failures. This gives Claude a result it can use to correct arguments or explain the next step.