Claude Connector Examples: 5 Patterns for Building Real Connectors (May 2026)
Five practical Claude Connector patterns with code examples.
TL;DR: The five Claude Connector examples that cover most real projects are read-only data tools, interactive MCP App UIs, write-action tools, REST API wrappers, and multi-resource connectors. Start with the smallest pattern that answers the user’s request, then add UI, auth, writes, and multiple resources only when the workflow needs them.
Claude Connectors are now a practical way to put company data and actions inside Claude through Remote MCP. The same MCP server can also power a ChatGPT App when you keep the core tool and resource contracts portable, because ChatGPT documents support for the MCP Apps UI bridge in addition to its Apps SDK compatibility layer.
This refresh keeps the original five examples, but updates the guidance for current Claude Connector and MCP App development: Streamable HTTP as the production default, text plus structuredContent as separate jobs, tool annotations that affect review and confirmation behavior, and repeatable tests before you submit or ship.
What changed since the first version
The architecture is still simple: an MCP server exposes tools, optional resources, and auth. The details matter more now because hosts are stricter and the ecosystem has moved toward portable MCP Apps.
- Claude custom connectors are Remote MCP servers. Build and deploy an HTTPS MCP endpoint, then connect it from Claude rather than assuming a local desktop-only flow.
- New and refreshed connectors should target Streamable HTTP. If an older connector still uses HTTP+SSE, plan the migration before directory submission or a broad rollout.
- Interactive UI should be treated as an MCP App resource, not a one-host widget. ChatGPT’s current Apps SDK docs recommend the MCP Apps bridge for new UI work and keep
window.openaifor compatibility and ChatGPT-specific extensions. - Tool annotations are no longer optional polish. They help hosts decide whether a tool is read-only, can modify data, or should trigger a confirmation step.
- Testing needs more than “it worked once in Claude.” You want handler tests, UI state tests, negative prompt tests, auth tests, and at least one real-host smoke test before launch.
If you are new to the terms, read What are Claude Connectors first. If you want a full build path, use the Claude Connectors tutorial. This post focuses on choosing the right connector shape for a real product.
The baseline connector shape
Most Claude Connectors have three layers:
- Tool definitions: names, descriptions, schemas, annotations, and optional resource links.
- Tool handlers: code that validates input, calls your service, and returns
content,structuredContent, or both. - Resource components: optional UI views that render the structured result in a sandboxed iframe.
Use text content for data the model should read. Use structuredContent for data your UI should render. Use _meta for widget-only values when the host supports it, such as IDs, cursors, or UI state that should not become model-visible text.
That split keeps your connector easier to test. A search tool can return three concise text bullets for Claude and a richer structuredContent.results array for the UI. The model gets enough context to answer, and the UI gets stable fields for rendering.
Pattern 1: Read-only data connector
This is the simplest useful connector. The tool fetches data, trims it to the part Claude needs, and returns text content. No UI. No resource. No client bundle.
Use this pattern for documentation search, policy lookup, log summaries, account facts, status checks, or any case where Claude’s answer is the interface.
// src/tools/search-docs.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
title: 'Search Documentation',
description:
'Search internal documentation by keyword. Use when the user asks about policies, runbooks, APIs, or engineering how-to guides.',
annotations: { readOnlyHint: true },
};
export const schema = {
query: z.string().min(2).describe('Search keywords or a natural language question'),
limit: z.number().int().min(1).max(10).optional().describe('Maximum results to return'),
};
export default async function (
args: { query: string; limit?: number },
_extra: ToolHandlerExtra
) {
const results = await searchDocuments(args.query, args.limit ?? 5);
return {
content: [
{
type: 'text' as const,
text: results
.map((doc) => [
`Title: ${doc.title}`,
`Summary: ${doc.summary}`,
`URL: ${doc.url}`,
].join('\n'))
.join('\n\n'),
},
],
};
}
Why this works:
- The tool name and description tell Claude when to call it.
- The schema accepts natural user phrasing instead of leaking your backend API shape.
- The handler returns short, model-readable text instead of a raw search API payload.
- The read-only annotation tells the host the tool does not change user data.
Avoid returning every matching document. Rank results server-side, include only fields Claude needs, and add pagination when users may need more. Large raw payloads make answers worse and make host limits easier to hit.
Pattern 2: Interactive connector with an MCP App UI
Add UI when text stops being the best interface. Dashboards, tables, approval cards, maps, timelines, and forms usually work better as interactive resources.
The tool points to a resource and returns structuredContent. The resource renders that data.
// src/tools/get-metrics.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
resource: 'metrics-dashboard',
title: 'Get Metrics',
description:
'Fetch product metrics for a time range. Use when the user asks for traffic, conversion, revenue, or error-rate performance.',
annotations: { readOnlyHint: true },
};
export const schema = {
period: z.enum(['today', '7d', '30d', '90d']).describe('Metrics period'),
};
export default async function (
args: { period: 'today' | '7d' | '30d' | '90d' },
_extra: ToolHandlerExtra
) {
const metrics = await fetchMetrics(args.period);
return {
content: [
{
type: 'text' as const,
text: `Revenue was ${metrics.revenueFormatted}, conversion was ${metrics.conversionRateFormatted}, and error rate was ${metrics.errorRateFormatted} for ${args.period}.`,
},
],
structuredContent: {
period: args.period,
pageViews: metrics.pageViews,
conversionRate: metrics.conversionRate,
revenue: metrics.revenue,
errorRate: metrics.errorRate,
sparkline: metrics.dailyPageViews,
},
};
}
// src/resources/metrics-dashboard/metrics-dashboard.tsx
import { SafeArea, useToolData } from 'sunpeak';
import type { ResourceConfig } from 'sunpeak';
type MetricsData = {
period: string;
pageViews: number;
conversionRate: number;
revenue: number;
errorRate: number;
sparkline: number[];
};
export const resource: ResourceConfig = {
description: 'Render product metrics as a compact dashboard',
};
export function MetricsDashboardResource() {
const { output: data } = useToolData<unknown, MetricsData>();
if (!data) return null;
return (
<SafeArea>
<section className="grid grid-cols-2 gap-3 p-4">
<MetricCard label="Page views" value={data.pageViews.toLocaleString()} />
<MetricCard label="Conversion" value={`${(data.conversionRate * 100).toFixed(1)}%`} />
<MetricCard label="Revenue" value={`$${data.revenue.toLocaleString()}`} />
<MetricCard label="Error rate" value={`${(data.errorRate * 100).toFixed(2)}%`} />
</section>
</SafeArea>
);
}
The important part is the dual result. content gives Claude a compact summary it can cite in the conversation. structuredContent gives the UI stable fields for rendering. If you only return UI data with no model-readable text, Claude may have less context for follow-up answers.
For portable UI, design against MCP Apps concepts first. ChatGPT documents the MCP Apps bridge as JSON-RPC over postMessage; Claude Connectors render MCP App resources in their own host runtime. If you need a host-only feature, hide it behind capability detection instead of building two separate apps.
Pattern 3: Write-action connector
Sooner or later a connector needs to change data. A support connector closes a ticket. A CRM connector logs a call. A project connector creates a task. Treat these as a separate pattern because they need clearer schemas, stricter testing, and more careful review.
Split reads from writes. Do not make one broad updateRecord tool that accepts arbitrary JSON. Give Claude narrow tools with concrete verbs.
// src/tools/search-tickets.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
resource: 'ticket-list',
title: 'Search Tickets',
description:
'Search support tickets by keyword, status, or assignee. Use before updating a ticket when the user has not provided an exact ticket ID.',
annotations: { readOnlyHint: true },
};
export const schema = {
query: z.string().optional().describe('Search keywords'),
status: z.enum(['open', 'in_progress', 'resolved', 'closed']).optional(),
assignee: z.string().email().optional().describe('Assignee email'),
};
export default async function (
args: { query?: string; status?: string; assignee?: string },
_extra: ToolHandlerExtra
) {
const tickets = await searchTickets(args);
return {
content: [
{
type: 'text' as const,
text: tickets.map((ticket) => `${ticket.id}: ${ticket.title} (${ticket.status})`).join('\n'),
},
],
structuredContent: { tickets },
};
}
// src/tools/resolve-ticket.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
resource: 'ticket-detail',
title: 'Resolve Ticket',
description:
'Mark one support ticket as resolved with a resolution note. Use only after the user confirms the exact ticket.',
annotations: {
readOnlyHint: false,
destructiveHint: true,
},
};
export const schema = {
ticketId: z.string().describe('The ticket ID to resolve'),
resolutionNote: z.string().min(10).describe('Short note explaining the resolution'),
};
export default async function (
args: { ticketId: string; resolutionNote: string },
_extra: ToolHandlerExtra
) {
const ticket = await resolveTicket(args.ticketId, args.resolutionNote);
return {
content: [
{
type: 'text' as const,
text: `Resolved ticket ${ticket.id}: ${ticket.title}.`,
},
],
structuredContent: {
ticket,
action: 'resolved',
},
};
}
Write tools need extra care:
- Put the record identifier in the schema. Do not rely on hidden UI state for the action target.
- Return the final state after the write. Users should see what changed.
- Test duplicate calls. A retry should not create two orders, two comments, or two charges.
- Add permission errors as first-class states. The UI should not look successful when the upstream API rejected the action.
If the write is reversible, still mark it honestly as a write. If it deletes, submits, sends, purchases, publishes, or changes someone else’s data, treat it as destructive and test the confirmation path.
Pattern 4: REST API wrapper
This pattern wraps an existing HTTP API as a connector. It is common because most teams already have the API; they need an MCP layer that turns product operations into model-friendly tools.
The mistake is exposing the API too literally. Claude should not choose arbitrary paths, methods, and request bodies. Give it a small set of product-level tools.
// src/tools/get-invoice.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
resource: 'invoice-card',
title: 'Get Invoice',
description:
'Fetch one invoice by invoice number. Use when the user asks about invoice status, amount, due date, or payment details.',
annotations: { readOnlyHint: true },
};
export const schema = {
invoiceNumber: z.string().describe('Invoice number, such as INV-1042'),
};
export default async function (
args: { invoiceNumber: string },
extra: ToolHandlerExtra
) {
const token = extra.authInfo?.token;
const response = await fetch(
`https://api.example.com/invoices/${encodeURIComponent(args.invoiceNumber)}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
if (response.status === 404) {
return {
content: [{ type: 'text' as const, text: `No invoice found for ${args.invoiceNumber}.` }],
structuredContent: { status: 'not_found', invoiceNumber: args.invoiceNumber },
};
}
if (!response.ok) {
throw new Error(`Invoice API returned ${response.status}`);
}
const invoice = await response.json();
return {
content: [
{
type: 'text' as const,
text: `Invoice ${invoice.number} is ${invoice.status}. Amount due: ${invoice.amountDueFormatted}. Due date: ${invoice.dueDate}.`,
},
],
structuredContent: {
number: invoice.number,
status: invoice.status,
amountDue: invoice.amountDue,
amountDueFormatted: invoice.amountDueFormatted,
dueDate: invoice.dueDate,
customerName: invoice.customer.name,
},
};
}
Keep these rules:
- One user intent per tool.
get-invoice,search-invoices, andsend-invoice-reminderare easier for a model to choose thancall-billing-api. - Transform upstream responses. Your MCP result should be smaller and cleaner than the raw API response.
- Put auth at the MCP boundary. For user-specific data, validate the user’s token before calling the upstream API.
- Model empty and error states. “No invoice found” is a useful result, not an exception.
- Avoid leaking internal fields. If the UI does not need it and Claude should not read it, leave it out or put host-supported widget-only values in
_meta.
This pattern is also the best place to add caching and rate-limit handling because your MCP server controls the upstream request.
Pattern 5: Multi-resource connector
A single MCP server can expose many tools and many resources. Use this when the domain has distinct views: search results, detail cards, timelines, settings, forms, and reports.
// src/tools/list-projects.ts
export const tool: AppToolConfig = {
resource: 'project-list',
title: 'List Projects',
description: 'List active projects with status, owner, due date, and risk level.',
annotations: { readOnlyHint: true },
};
// src/tools/get-project.ts
export const tool: AppToolConfig = {
resource: 'project-detail',
title: 'Get Project Details',
description: 'Get full details for one project by ID, including owner, milestones, blockers, and recent activity.',
annotations: { readOnlyHint: true },
};
// src/tools/get-project-timeline.ts
export const tool: AppToolConfig = {
resource: 'project-timeline',
title: 'Get Project Timeline',
description: 'Show milestone timing, dependencies, and late work for one project.',
annotations: { readOnlyHint: true },
};
The file structure stays predictable:
src/
resources/
project-list/
project-list.tsx
project-detail/
project-detail.tsx
project-timeline/
project-timeline.tsx
tools/
list-projects.ts
get-project.ts
get-project-timeline.ts
Each resource should have one job. A list resource should not also be a timeline editor. A detail resource should not also handle bulk triage. Small resources are easier to test across hosts, themes, and display modes.
You can still reuse components under the hood. A StatusBadge, OwnerAvatar, or EmptyState component can appear in every resource. Keep the resource entry points focused, and share smaller UI pieces inside them.
Choosing the right pattern
Use the smallest pattern that gives the user a good result.
| User need | Best starting pattern | Why |
|---|---|---|
| ”Find the policy for contractor access” | Read-only data connector | Claude should read and summarize text. |
| ”Show this quarter’s revenue by segment” | Interactive connector with UI | The user needs to scan and compare values. |
| ”Close ticket SUP-421 with this note” | Write-action connector | The workflow changes data and needs confirmation. |
| ”Let Claude query our billing system” | REST API wrapper | Existing API endpoints become focused MCP tools. |
| ”Show projects, details, and timelines” | Multi-resource connector | Different requests deserve different UI views. |
Most production connectors combine patterns. A CRM connector might use read-only search, detail resources, and write-action tools. A finance connector might wrap REST APIs, render invoice cards, and include approval forms. The point is to name the parts instead of building one large, hard-to-test connector.
Testing the examples before you ship
Testing is where connector examples become production code. The minimum useful test set is:
- Handler unit tests for schemas, empty states, auth, upstream errors, and output shape.
- UI tests for loading, success, empty, error, and permission-denied states.
- Tool-selection tests with golden prompts, including negative prompts that should not call your tool.
- Cross-host rendering checks if you target both Claude and ChatGPT.
- One real-host smoke test after the server is reachable over HTTPS.
With sunpeak’s MCP testing framework, you can run local inspector tests against replicated ChatGPT and Claude runtimes without paid host accounts or AI credits. For any existing MCP server, use:
npx sunpeak test init --server https://your-server.example.com/mcp
npx sunpeak test
For a sunpeak project, keep simulation fixtures near the resource or test that uses them:
{
"title": "Invoice overdue",
"input": {
"invoiceNumber": "INV-1042"
},
"output": {
"content": [
{
"type": "text",
"text": "Invoice INV-1042 is overdue. Amount due: $4,200. Due date: 2026-05-01."
}
],
"structuredContent": {
"number": "INV-1042",
"status": "overdue",
"amountDue": 4200,
"amountDueFormatted": "$4,200",
"dueDate": "2026-05-01",
"customerName": "Acme Co"
}
}
}
Then write Playwright assertions against the rendered resource and protocol assertions against the raw tool result. That catches both classes of bugs: the server returned the wrong shape, or the UI failed to handle a valid shape.
Submission and review checklist
Before you submit a connector or put it in front of internal users, check the parts reviewers and users will hit first:
- Every tool has a human-readable
title, specificdescription, tight schema, and honest annotations. - Read and write actions are separate tools.
- OAuth works from a clean browser session, including denied consent and expired tokens.
- The connector returns useful empty states instead of generic errors.
- Interactive resources work in narrow and wide layouts, light and dark themes, and slow-loading states.
- External links are expected, declared when the host asks for them, and safe to open.
- Your privacy policy and support contact match what the connector actually does with user data.
- You can replay the same test states in CI.
For Claude-specific review details, read the current Claude Connector Directory submission guide. For ChatGPT-specific UI behavior, read the OpenAI Apps SDK reference and Build your ChatGPT UI.
Where sunpeak fits
You can build any of these patterns with a plain MCP SDK. sunpeak helps when you want the same connector to be easier to build, inspect, and test across hosts.
Use npx sunpeak new when you want a full MCP App project with tools, resources, simulations, local inspector, and tests wired up. Use npx sunpeak inspect --server URL when you already have an MCP server and want to inspect it locally. Use npx sunpeak test init --server URL when you want CI-friendly tests for an existing connector.
That matters for Claude Connectors because most bugs are host-state bugs: a tool result is valid but too large, a widget works in one display mode but not another, auth fails only after token refresh, or Claude and ChatGPT render the same resource with different host context. Repeatable tests make those bugs visible before users find them.
Start with the simplest connector pattern that solves the job. Add UI when users need to see or act on structured data. Add write tools only when the workflow truly needs them. Add more resources when one UI starts doing too many jobs. That path keeps your Claude Connector understandable as it grows.
Get Started
npx sunpeak new
Further Reading
- Claude Connectors tutorial - build and deploy a connector from scratch
- Designing Claude Connector tools - schemas, descriptions, and reliable tool calls
- Claude Connector data access patterns - text, structuredContent, and pagination
- Claude Connector Directory submission - current review checklist
- Migrate your Claude Connector from SSE to Streamable HTTP
- Claude Connector Framework - sunpeak overview
- sunpeak MCP testing framework - local inspector, E2E tests, visual tests, and evals
- OpenAI Apps SDK reference - portable MCP Apps bridge and ChatGPT extensions
- Claude custom connectors via Remote MCP - official Claude Help Center guide
- MCP tools specification - official tool result and annotation reference
Frequently Asked Questions
What are the most common Claude Connector examples?
The most common Claude Connector examples are read-only search connectors, interactive dashboard connectors, write-action connectors for tickets or CRM records, REST API wrapper connectors, and multi-resource connectors with list, detail, and timeline views. Each pattern uses the same MCP server foundation: tools expose actions, resources expose optional UI, and tool results decide what Claude and the UI can read.
Should a Claude Connector return text content or structuredContent?
Return text content when Claude should read and summarize the result in the conversation. Return structuredContent when a UI component should render stable data such as rows, cards, charts, forms, or status objects. Many production connectors return both: concise text for the model and structuredContent for the embedded UI.
Can Claude Connectors render interactive UI?
Yes. Interactive Claude Connectors use MCP Apps resources to render UI in a sandboxed iframe. The tool returns structuredContent and a resource link, then the host renders the resource and sends the tool result to the UI. The same MCP Apps pattern can also run in ChatGPT when you build against portable MCP Apps APIs.
How should I design write tools in a Claude Connector?
Write tools should be small, explicit, and easy to confirm. Split read tools from write tools, use clear schemas, return a post-action summary, and mark tool annotations honestly. Use readOnlyHint for tools that only read data, and use destructiveHint for tools that can delete, overwrite, submit, send, or otherwise change user data.
Can a Claude Connector also work as a ChatGPT App?
Yes, if you keep the core tool and resource contracts portable. Claude Connectors and ChatGPT Apps both sit on MCP. ChatGPT documents the MCP Apps UI bridge and Apps SDK compatibility layer, while Claude Connectors use Remote MCP. Build shared tools and resources first, then isolate host-specific behavior behind capability checks.
How do I test Claude Connector examples locally?
Use a local inspector and deterministic simulation data. With sunpeak, run pnpm dev for a local ChatGPT and Claude runtime replica, or run npx sunpeak inspect --server URL against any MCP server. Add Playwright tests for UI states, unit tests for handlers, and golden prompts for tool selection before testing against the real host.
What should I check before submitting a Claude Connector?
Before submission, verify that every tool has a clear title, description, schema, and annotations; authenticated connectors complete OAuth; interactive connectors have stable screenshots and accessible UI; external links are declared when needed; and the connector behaves predictably on empty, error, and permission-denied states.