Skip to main content
All posts

Claude Connectors Tutorial: Build and Deploy a Connector to Claude (October 2026)

Abe Wheeler
Claude ConnectorsClaude AppsMCP AppsMCP App FrameworkChatGPT AppsChatGPT App FrameworkClaude Connector FrameworkClaude Connector Testing
Building an interactive Claude Connector.

Building an interactive Claude Connector.

TL;DR: A Claude Connector is an MCP server. Build a tool that returns useful text and structured data, link an optional MCP App resource for interactive UI, test locally, then connect its public HTTPS /mcp endpoint through Claude’s Customize > Connectors page.

Claude Connectors are MCP servers that extend what Claude can do. The Connectors Directory includes verified and community connectors, while custom connectors let teams point Claude at their own remote MCP servers. Some connectors only return data for Claude to reason over. Others surface MCP App UI inside the chat, complete with cards, charts, forms, and action buttons.

In this tutorial, you’ll build an interactive connector that renders a support ticket card inside Claude, test it locally across multiple hosts, and connect it to a real Claude session. The examples use sunpeak because it gives you a file-based MCP App project, local ChatGPT and Claude runtimes, simulation fixtures, and Playwright test fixtures in one scaffold.

Two Types of Connectors

Before building, it helps to understand the two types. (For a deeper comparison, see Claude Connectors vs Claude Apps.)

Standard connectors expose tools that return data. Claude calls the tool, gets structured data back, and uses it in a text response. A standard connector linking Claude to your issue tracker would let Claude look up tickets and describe them in prose.

Interactive connectors also include MCP App resources: UI templates that render inside the chat. Instead of describing a ticket in text, Claude renders your component as a visual card with status badges, assignee avatars, and action buttons.

Both types are MCP servers. An interactive connector is a standard connector with a UI layer on top. This tutorial builds the interactive kind.

Anatomy of an Interactive Connector

An interactive Claude Connector has three parts:

  1. A tool that Claude calls. The tool has a schema (what arguments it accepts) and a handler (what it does when called). The handler fetches data from your service and returns structured content.
  2. A resource (UI component) that renders the tool’s output inside the conversation. This is a React component that receives the tool’s structured content and displays it as a card, chart, form, or whatever UI makes sense.
  3. A simulation (optional, for testing). A JSON fixture that defines a reproducible tool state, so you can develop and test your UI without calling the real backend every time.

At the protocol level, a tool’s _meta.ui.resourceUri points to a ui:// HTML resource. The host fetches that resource, renders it in a sandboxed iframe, and sends tool input and results to the view. A tool result can contain both content for the model and structuredContent for the UI. The MCP Apps architecture spells out that sequence. In sunpeak, the resource: 'ticket' field below generates the linkage for you.

Make the tool useful without UI first, then add the resource when the user needs to inspect, compare, edit, confirm, or navigate structured information. Return concise text alongside structured data so the answer still works when a client does not render MCP Apps. In the ticket example, Claude should be able to report the ticket’s status and priority even if the card never opens.

Prerequisites

You need Node.js 20 or later and pnpm. You do not need a Claude account for local development.

Transport Details That Matter Now

Remote Claude Connectors should use Streamable HTTP. The practical shape is simple: expose one MCP endpoint, usually /mcp, and handle POST requests for client messages. If your server supports streaming, POST and GET can return text/event-stream; otherwise POST can return JSON.

Three details are easy to miss when you read older MCP examples:

  • Validate the Origin header to protect local and private-network servers from DNS rebinding attacks.
  • Preserve MCP-Session-Id if your transport keeps per-client session state.
  • Handle MCP-Protocol-Version after initialization so newer hosts and SDKs can negotiate cleanly.

sunpeak handles the default transport plumbing for scaffolded apps. If you are wiring your own server, test those headers directly before you try the connector in Claude.

Step 1: Scaffold the Project

This tutorial uses sunpeak to scaffold the project because it sets up the full MCP server structure (tools, resources, simulations, inspector) in one command:

npx sunpeak new

Name your project and pick any starter resources. We’re building a new resource from scratch, so the selection doesn’t matter. cd into your project directory.

Step 2: Build the Resource (UI)

Create src/resources/ticket/ticket.tsx:

import { useToolData, SafeArea } from 'sunpeak';
import type { ResourceConfig } from 'sunpeak';

export const resource: ResourceConfig = {
  title: 'Ticket',
  description: 'Display a support ticket',
};

interface TicketData {
  id: string;
  title: string;
  status: 'open' | 'in_progress' | 'resolved';
  priority: 'low' | 'medium' | 'high';
  assignee: string;
  created: string;
  description: string;
}

const statusColors = {
  open: 'bg-yellow-100 text-yellow-800',
  in_progress: 'bg-blue-100 text-blue-800',
  resolved: 'bg-green-100 text-green-800',
};

const priorityColors = {
  low: 'bg-gray-100 text-gray-700',
  medium: 'bg-orange-100 text-orange-700',
  high: 'bg-red-100 text-red-700',
};

export function TicketResource() {
  const { output } = useToolData<unknown, TicketData>(undefined, undefined);

  if (!output) return null;

  return (
    <SafeArea className="p-5 font-sans max-w-md mx-auto">
      <div className="flex items-start justify-between mb-3">
        <div>
          <span className="text-xs text-gray-400 font-mono">{output.id}</span>
          <h1 className="text-lg font-bold mt-0.5">{output.title}</h1>
        </div>
        <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${priorityColors[output.priority]}`}>
          {output.priority}
        </span>
      </div>

      <p className="text-sm text-gray-600 mb-4">{output.description}</p>

      <div className="flex items-center gap-3 text-sm">
        <span className={`px-2 py-0.5 rounded-full text-xs font-medium ${statusColors[output.status]}`}>
          {output.status.replace('_', ' ')}
        </span>
        <span className="text-gray-400">|</span>
        <span className="text-gray-600">{output.assignee}</span>
        <span className="text-gray-400">|</span>
        <span className="text-gray-400">{output.created}</span>
      </div>
    </SafeArea>
  );
}

The component receives ticket data via useToolData and renders it as a card with status and priority badges. SafeArea handles padding so the content doesn’t overlap with host UI chrome. See the resource docs for all config options.

sunpeak provides 20+ typed React hooks for building resources. Beyond useToolData, you can use useHostContext to detect which host your app is running in, useDisplayMode to adapt your layout to the host’s display mode, useTheme to pick up light/dark theming, and useCallServerTool to call back to your MCP server from the UI. See the interactive MCP Apps guide for patterns using useAppState and other hooks.

Step 3: Build the Tool (Backend)

Create src/tools/show-ticket.ts:

import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';

export const tool: AppToolConfig = {
  resource: 'ticket',
  title: 'Show Ticket',
  description: 'Look up a support ticket and display it',
  annotations: { readOnlyHint: true },
};

export const schema = {
  ticketId: z.string().describe('Ticket ID to look up (e.g. TICK-1234)'),
};

type Args = z.infer<z.ZodObject<typeof schema>>;

export default async function (args: Args, _extra: ToolHandlerExtra) {
  // Replace this fixture with a lookup in your ticket system.
  // Reject unknown IDs instead of returning a different ticket.
  if (args.ticketId !== 'TICK-1234') {
    return {
      content: [{ type: 'text' as const, text: `Ticket ${args.ticketId} was not found.` }],
      isError: true,
    };
  }

  const ticket = {
    id: args.ticketId,
    title: 'Search results not loading on mobile',
    status: 'in_progress',
    priority: 'high',
    assignee: 'Sarah Chen',
    created: '2026-03-04',
    description:
      'Users on iOS Safari report that search results fail to render after the latest deploy.',
  };

  return {
    content: [{ type: 'text' as const, text: `${ticket.id}: ${ticket.title}. Status: ${ticket.status}; priority: ${ticket.priority}.` }],
    structuredContent: ticket,
  };
}

The resource: 'ticket' field links this tool to the ticket resource. Claude gets a short, readable result, while the card gets the full ticket object. The example checks ticketId before returning data so a request for another ID cannot silently display TICK-1234. In production, fetch the requested record and check the caller’s access before returning it.

The annotations field matters for model behavior and directory review. Use readOnlyHint: true only for tools that fetch or compute information without changing state. For write tools, set readOnlyHint: false, then use destructiveHint and openWorldHint to describe the effect accurately. Treat annotations as descriptions, not access control: your server still has to authorize every call. See the Claude tool design guide for naming and read/write boundaries.

If your connector needs to call external APIs from the resource iframe (for example, loading images from a CDN), you’ll need to configure CSP domains. See the CSP guide for how to set up resourceDomains, frameDomains, and connectDomains.

See the tool docs for all config options.

Step 4: Add a Simulation (Test Data)

Create tests/simulations/show-ticket.json:

{
  "tool": "show-ticket",
  "userMessage": "Show me ticket TICK-1234",
  "toolInput": {
    "ticketId": "TICK-1234"
  },
  "toolResult": {
    "structuredContent": {
      "id": "TICK-1234",
      "title": "Search results not loading on mobile",
      "status": "in_progress",
      "priority": "high",
      "assignee": "Sarah Chen",
      "created": "2026-03-04",
      "description": "Users on iOS Safari report that search results fail to render after the latest deploy. Affects approximately 12% of mobile traffic."
    }
  }
}

Simulations are JSON fixtures that define a reproducible tool state: the tool input Claude would send and the output your handler would return. The inspector loads them automatically so you can develop your UI against known data without calling the real backend or spending host credits. Create multiple simulations per tool to cover success, empty, permission denied, expired token, timeout, and large-result states. You can load the same simulations in Playwright tests, which keeps manual QA and CI pointed at the same examples. For a complete walkthrough of simulations, see the MCP App tutorial.

Step 5: Test Locally

pnpm dev

Open http://localhost:3000. The sunpeak inspector opens with your connector running. Select Claude from the Host dropdown in the sidebar. Your ticket card renders inside Claude’s conversation chrome, with host-specific theme, viewport, display mode, and safe-area behavior applied.

Switch to ChatGPT in the dropdown to verify it works there too. The same component renders in both hosts because both implement the MCP App standard. This is one of the main advantages of building on the MCP standard: your connector works across hosts without code changes.

No Claude account is needed for this local loop. The local inspector replicates both runtimes on localhost, so you can iterate without spending credits or clicking through manual refresh flows.

Step 6: Write Automated Tests

Before connecting to a real Claude session, add automated tests so you can catch regressions on every code change. sunpeak includes a full testing framework with several test layers:

E2E tests use the inspector Playwright fixture to render your resource in a real browser:

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

test('renders ticket card', async ({ inspector }) => {
  const result = await inspector.renderTool('show-ticket', {});
  const app = result.app();
  await expect(app.locator('text=TICK-1234')).toBeVisible();
  await expect(app.locator('text=high')).toBeVisible();
});

Tests run against both ChatGPT and Claude host replicas via Playwright projects, so you get cross-host coverage before you deploy. Add unit tests for tool handlers, visual regression tests for UI drift, security tests for auth and input handling, and multi-model evals to verify that different LLMs call your tools correctly.

Run the full test suite:

pnpm test

This runs unit tests and browser tests. For more granular control, use pnpm test:unit, pnpm test:e2e, pnpm test:visual, pnpm test:live, or pnpm test:eval depending on the project template. Add an assertion for an unknown ticket ID and another for a denied ticket, since a passing card snapshot alone cannot prove correct access control. See the testing guide for a full walkthrough of all test types.

Step 7: Connect to a Real Claude Session

When you’re ready to test in a real Claude session, you need a publicly accessible HTTPS URL. Claude cannot reach localhost directly.

Create a tunnel

Use ngrok or Cloudflare Tunnel to expose your local server:

ngrok http 8000

Copy the forwarding URL, such as https://abc123.ngrok-free.app. Your connector URL should point at the MCP endpoint, usually https://abc123.ngrok-free.app/mcp.

Add the custom connector in Claude

  1. Open Claude Connectors.
  2. Choose Add > Custom > Web.
  3. Give the connector a name and enter your tunnel URL with the /mcp path: https://abc123.ngrok-free.app/mcp.
  4. Review the authentication settings Claude detected, then choose whether users sign in now, when needed, or not at all. Complete the setup and enable the connector in a conversation.

Anthropic’s connector setup guide has the current menu path. A free account can add one custom connector; organization access can depend on admin settings. Test with the same account type and permissions your users will have, especially if the connector needs OAuth or sensitive scopes.

Use it in a conversation

Ask Claude: “Show me ticket TICK-1234.”

Claude calls your show-ticket tool, and your ticket card renders inside the chat. Keep your terminal and MCP server logs open while you test. The first real-host run should confirm initialization, tool listing, tool call input, structured output, resource fetches, auth challenges, and any CSP errors. For more on live testing, see live testing Claude Connectors in ChatGPT.

Then ask for a ticket ID that does not exist. The tool should say it was not found, and it should never show the fixture ticket. Repeat with a user who lacks permission to see TICK-1234. This catches a more serious class of mistake than a card that merely fails to render: returning another user’s data under a valid-looking UI. In your logs, avoid recording ticket bodies or OAuth tokens while you trace the request.

How Claude handles resource bundles

Claude’s iframe sandbox blocks HTTP script sources, which means it cannot load resources from a local Vite dev server. When Claude fetches your resource, it needs self-contained HTML with the JavaScript bundled in.

sunpeak handles this automatically: it detects Claude’s user-agent and serves the pre-built production bundle. When you save a file change during development, sunpeak auto-rebuilds and sends a notifications/resources/list_changed notification so Claude re-fetches the updated resource. If you’re building without sunpeak, your server needs to handle this same pattern.

Step 8: Submit to the Connectors Directory

To distribute your connector to Claude users beyond custom install, submit it to the Connectors Directory.

Requirements

Before submitting, check these requirements:

  • Transport: Streamable HTTP. Your server must be internet-accessible over HTTPS. SSE transport was deprecated in the March 2025 MCP spec, and new connector work should use Streamable HTTP. If you’re migrating from SSE, see the SSE to Streamable HTTP migration guide.
  • Annotations: Every tool should include a clear title and accurate annotations. Use readOnlyHint for read-only tools, destructiveHint for irreversible write actions, and openWorldHint for actions that can affect public internet state.
  • Result size: Claude’s server guide lists roughly 150,000 characters per tool result for claude.ai and Desktop. Claude Code uses a separate 25,000-token default that can be configured. Keep model-facing text short and paginate large result sets.
  • Timeout: Claude.ai and Desktop currently allow 240 seconds per tool call. Claude Code’s timeout is configurable. Test your slowest tool below the limit with room for network delay.
  • Auth: If your connector requires authentication, use OAuth with user consent. Provide test credentials for reviewers, and avoid MFA or private-network requirements in the review account. Pure client credentials flow is not a substitute for user consent in Claude.
  • OAuth callbacks: Hosted Claude connectors use https://claude.ai/api/mcp/auth_callback. If you support Claude Code, allow port-agnostic loopback redirects for http://localhost/callback and http://127.0.0.1/callback.
  • Screenshots: For MCP Apps, include 3 to 5 PNG screenshots of the app response, each at least 1000 pixels wide, with prompt text supplied separately. Do not include the prompt in the image.
  • Support and policy: Provide support contact details, clear user documentation, and a privacy policy if the connector handles user data.

Annotations example

// Read-only tool
export const tool: AppToolConfig = {
  resource: 'ticket',
  title: 'Show Ticket',
  description: 'Look up a support ticket and display it',
  annotations: { readOnlyHint: true },
};
// Destructive tool
export const tool: AppToolConfig = {
  resource: 'ticket',
  title: 'Delete Ticket',
  description: 'Permanently delete a support ticket',
  annotations: {
    readOnlyHint: false,
    destructiveHint: true,
    openWorldHint: false,
  },
};

Pre-submission testing

Before you submit, run your full local test suite, then test the same HTTPS server as a real custom connector in Claude. The pre-submission testing checklist covers what reviewers look for, including annotation coverage, token and size limits, timeout behavior, OAuth flow correctness, privacy policy links, and screenshots.

Submit

Submit a remote server at claude.ai/directory/manage and choose MCP connector. Anthropic’s current submission guide says anyone on an eligible paid plan can submit. It also says local MCP Bundles are no longer accepted as standalone directory listings, so package a local server in a Claude plugin if that is your distribution route. The guide lists the current review flow and required assets; the tool design guide covers schemas and read/write boundaries.

Your Connector Works Everywhere

The connector you just built is an MCP server. Its tools can work with any MCP-compatible host that can reach the server and satisfy its auth requirements. Its UI resources work in hosts that implement MCP Apps.

ChatGPT calls the MCP-backed UI surface an “App” rather than a “Connector.” OpenAI’s current docs describe Apps as the MCP-backed capability inside a Plugin, and Plugins are the package users discover, install, submit, and publish. That means the same server architecture can support Claude Connectors and ChatGPT Apps, but each host has its own submission, auth callback, tool-annotation, and review rules. For a deeper look at building for ChatGPT specifically, see the ChatGPT App tutorial.

To verify cross-host rendering automatically, write Playwright tests that load your simulations in the inspector:

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

test('renders ticket card', async ({ inspector }) => {
  const result = await inspector.renderTool('show-ticket', {});
  const app = result.app();
  await expect(app.locator('text=TICK-1234')).toBeVisible();
});

Tests run against both host replicas, so you get cross-host coverage without writing separate test suites. See the testing guide for a full walkthrough, or get started with sunpeak’s testing framework.

Get Started

Documentation →
npx sunpeak new

Further Reading

Frequently Asked Questions

How do I build a Claude Connector in 2026?

A Claude Connector is an MCP server that exposes tools, prompts, resources, and optional MCP App UI to Claude. Build a remote MCP server, expose it over HTTPS with Streamable HTTP, add tools with clear schemas and safety annotations, and register MCP App resources when a visual UI helps the user inspect or act on structured data.

What is the difference between a standard Claude Connector and an interactive one?

A standard connector exposes MCP tools and data to Claude. An interactive connector also links a tool to a ui:// resource with MCP App metadata, so Claude can render a card, form, or other view in a sandboxed iframe. The tool should still return useful text when a host cannot render the UI.

How do I connect my MCP server to Claude?

Expose your remote MCP server at a public HTTPS URL, commonly ending in /mcp. In Claude, open Customize > Connectors, choose Add > Custom > Web, enter the URL, and review the detected authentication settings. A tunnel can expose a local development server for testing.

Do I need a paid Claude account to develop a Claude Connector?

No. You can build and test the connector locally without connecting it to Claude. sunpeak includes a multi-host inspector that replicates ChatGPT and Claude app runtimes at localhost, so you can verify tools, resources, themes, display modes, and simulations before using a real host. You need Claude access only when you want to test the deployed or tunneled server inside Claude.

How do I submit my connector to the Claude Connectors Directory?

On an eligible paid Claude plan, submit a remote MCP server through the developer portal at claude.ai/directory/manage and choose MCP connector. Prepare a public HTTPS endpoint, tool annotations, docs, privacy policy, support contact, a populated reviewer account, and 3 to 5 PNG screenshots if the connector includes MCP App UI. Local MCP bundles are no longer accepted as standalone directory listings.

Does my Claude Connector also work in ChatGPT and other hosts?

The server-side tools are MCP, so they can work across MCP-compatible hosts. Interactive UI depends on MCP App support in each host. ChatGPT Apps are now submitted and published as Plugins, while Claude exposes the same broad pattern through Connectors and MCP Apps. Build the tool contract first, then add UI resources that degrade gracefully.

Why does Claude need a pre-built bundle instead of Vite HMR?

Claude's iframe sandbox blocks HTTP script sources, so it cannot load from a local Vite dev server. Your framework needs to detect Claude's user-agent and serve a pre-built production bundle instead. On file changes, the server sends a notifications/resources/list_changed notification so Claude re-fetches the resource. sunpeak handles this automatically.

What are the requirements for the Claude Connectors Directory?

Directory connectors need a remote HTTPS server, working authentication where needed, clear tool names and annotations, docs, privacy and support details, and a test account. Claude.ai and Desktop currently cap a tool result at about 150,000 characters and a tool call at 240 seconds. Claude Code has separate configurable limits. Check the official submission guide again before sending your listing.