Claude Connector Examples: 5 Patterns for Building Real Connectors
Five practical Claude Connector patterns with code examples.
TL;DR: Five Claude Connector patterns with code: read-only data connectors, interactive UI connectors, CRUD connectors, REST API wrappers, and multi-resource connectors. Each pattern shows the tool definition, handler, and resource component so you can pick the right architecture for your use case.
You know what a Claude Connector is. You have read the tutorial. Now you need to build one for your actual use case, and the architecture depends on what kind of data you are working with and how users should interact with it.
This post covers five patterns that cover most real-world connectors. Each includes enough code to show the structure, not just the concept. All examples use sunpeak and work in both Claude and ChatGPT.
Pattern 1: Read-Only Data Connector
The simplest connector pattern. Your tool fetches data and returns it as text. Claude weaves that text into its response. No UI, no structuredContent, just data in and text out.
This pattern works well when the data is best consumed as part of a conversation. Think internal documentation search, log lookups, configuration queries, or simple API fetches where Claude’s natural language response is the right 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. Returns matching ' +
'document titles, summaries, and URLs. Use when the user asks ' +
'about internal processes, policies, or how-to guides.',
annotations: { readOnlyHint: true },
};
export const schema = {
query: z.string().describe('Search keywords'),
limit: z.number().optional().describe('Max results to return (default 5)'),
};
export default async function (
args: { query: string; limit?: number },
_extra: ToolHandlerExtra
) {
const results = await searchDocuments(args.query, args.limit ?? 5);
// Return text content — Claude formats this into its response
return {
content: [
{
type: 'text' as const,
text: results
.map(
(doc) =>
`**${doc.title}**\n${doc.summary}\nURL: ${doc.url}`
)
.join('\n\n'),
},
],
};
}
No resource component needed. Claude gets the text, reasons over it, and presents the relevant parts to the user. This is how most Connectors Directory integrations work: Google Drive, Gmail, and Notion all return text that Claude summarizes.
When to use this pattern:
- The answer fits naturally in Claude’s conversational response
- Users do not need to interact with the data (no buttons, forms, or filters)
- The data is small enough to fit within the 25,000 token tool result limit
- You want the fastest path to a working connector
Pattern 2: Interactive Connector with UI
When your data benefits from visual presentation, add a resource component. The tool returns structuredContent instead of text, and your React component renders it as a card, chart, dashboard, or any other UI.
This is the “Claude App” pattern. It is what Figma, Canva, Asana, and other first-party interactive connectors use in the Connectors Directory.
// 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 performance metrics for a given time range. Returns ' +
'a dashboard with page views, conversion rate, revenue, and ' +
'error rate. Use when the user asks about site performance or analytics.',
annotations: { readOnlyHint: true },
};
export const schema = {
period: z
.enum(['today', '7d', '30d', '90d'])
.describe('Time period for metrics'),
};
export default async function (
args: { period: string },
_extra: ToolHandlerExtra
) {
const metrics = await fetchMetrics(args.period);
return {
structuredContent: {
period: args.period,
pageViews: metrics.pageViews,
conversionRate: metrics.conversionRate,
revenue: metrics.revenue,
errorRate: metrics.errorRate,
sparkline: metrics.dailyPageViews,
},
};
}
The resource component receives this data through useToolData():
// src/resources/metrics-dashboard/metrics-dashboard.tsx
import { useToolData, SafeArea } 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: 'Visualize performance metrics as a dashboard',
};
export function MetricsDashboardResource() {
const { output: data } = useToolData<unknown, MetricsData>();
if (!data) return null;
return (
<SafeArea>
<div className="grid grid-cols-2 gap-4 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)}%`}
alert={data.errorRate > 0.01}
/>
</div>
</SafeArea>
);
}
The key difference from Pattern 1: the tool config has a resource field pointing to the resource directory name, and the handler returns structuredContent instead of content. Claude renders your React component in an iframe inside the conversation, and the user sees a real dashboard instead of text.
When to use this pattern:
- Data is better consumed visually (charts, tables, status indicators)
- Users need to scan or compare multiple values at once
- You want your connector to stand out in the Connectors Directory with the “Interactive” badge
- The data structure is stable enough to design a UI for
Pattern 3: CRUD Connector
Most real connectors need both read and write operations. A project tracker needs search, get details, create, and update. A CRM needs lookup, log activity, and change status. This pattern shows how to structure a connector with mixed read/write tools.
The key difference from read-only connectors is the annotations field. Read tools use readOnlyHint: true. Write tools use destructiveHint: true. These annotations tell Claude whether to ask for user confirmation before calling the tool, and they are required for the Connectors Directory.
// 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. ' +
'Returns a list of matching tickets with ID, title, status, ' +
'and priority.',
annotations: { readOnlyHint: true },
};
export const schema = {
query: z.string().optional().describe('Search keywords'),
status: z
.enum(['open', 'in_progress', 'resolved', 'closed'])
.optional()
.describe('Filter by ticket status'),
assignee: z.string().optional().describe('Filter by assignee email'),
};
export default async function (
args: { query?: string; status?: string; assignee?: string },
_extra: ToolHandlerExtra
) {
const tickets = await searchTickets(args);
return { structuredContent: { tickets } };
}
// src/tools/update-ticket-status.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
resource: 'ticket-detail',
title: 'Update Ticket Status',
description:
'Change the status of a support ticket. Use when the user ' +
'asks to close, reopen, or change the state of a ticket.',
annotations: { destructiveHint: true },
};
export const schema = {
ticketId: z.string().describe('The ticket ID to update'),
status: z
.enum(['open', 'in_progress', 'resolved', 'closed'])
.describe('The new status'),
};
export default async function (
args: { ticketId: string; status: string },
_extra: ToolHandlerExtra
) {
const updated = await updateTicketStatus(args.ticketId, args.status);
return {
structuredContent: {
ticket: updated,
action: 'status_updated',
},
};
}
Notice that the search tool points to ticket-list and the update tool points to ticket-detail. Different tools can render different resource components. The search shows a list view, and the update shows the detail view with the updated status. See Pattern 5 for more on this.
When to use this pattern:
- Your service has both read and write operations
- Users need to take action through Claude (create records, update status, send messages)
- You want Claude to confirm destructive actions before executing them
- The connector wraps a task management, CRM, or workflow system
Pattern 4: REST API Wrapper
This pattern wraps an existing REST API as a Claude Connector. If you have a service with a REST API and want Claude to access it, this is the fastest path. Each API endpoint becomes an MCP tool.
The pattern works for any HTTP API: your own internal services, third-party SaaS APIs, or public APIs. The tool handler makes the HTTP request and translates the response into either text content or structuredContent.
// src/tools/get-weather.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
resource: 'weather-card',
title: 'Get Weather',
description:
'Get current weather conditions for a city. Returns ' +
'temperature, humidity, wind speed, and conditions.',
annotations: { readOnlyHint: true },
};
export const schema = {
city: z.string().describe('City name (e.g., "San Francisco")'),
units: z
.enum(['metric', 'imperial'])
.optional()
.describe('Temperature units (default: metric)'),
};
export default async function (
args: { city: string; units?: string },
_extra: ToolHandlerExtra
) {
const units = args.units ?? 'metric';
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${encodeURIComponent(args.city)}`
);
const data = await response.json();
return {
structuredContent: {
city: data.location.name,
country: data.location.country,
tempC: data.current.temp_c,
tempF: data.current.temp_f,
condition: data.current.condition.text,
humidity: data.current.humidity,
windKph: data.current.wind_kph,
icon: data.current.condition.icon,
units,
},
};
}
The key decisions when wrapping a REST API:
-
One tool per endpoint, not one tool for everything. A tool called “call API” with a
methodandpathparameter forces Claude to guess your API structure. Instead, create focused tools likeget-weather,search-locations, andget-forecastthat map to specific endpoints. Claude matches tool names and descriptions to user intent, so specific tools get called correctly. -
Transform the response. Do not pass raw API responses through as structuredContent. Pick the fields your resource component needs. This keeps the token payload small (the 25,000 token limit applies to the full tool response) and gives your UI component a clean data shape to work with.
-
Handle auth in the handler. If the API needs an API key, use environment variables. If it needs per-user OAuth, see Claude Connector authentication. Your tool handler is responsible for making authenticated requests to the upstream API.
When to use this pattern:
- You already have a REST API and want Claude to access it
- You are wrapping a third-party SaaS API (Stripe, GitHub, Jira, etc.)
- The API has clear, distinct endpoints that map to user intents
- You want to add Claude access without changing your existing API
Pattern 5: Multi-Resource Connector
A single connector can serve multiple UI views. Each tool points to a different resource, so the same MCP server renders different components depending on what the user asked for.
This pattern is common for connectors that present data at multiple levels of detail: a list view and a detail view, a summary and a full report, or a search results page and an individual result card.
// src/tools/list-projects.ts
export const tool: AppToolConfig = {
resource: 'project-list', // renders the list UI
title: 'List Projects',
description: 'List all active projects with status and owner.',
annotations: { readOnlyHint: true },
};
// src/tools/get-project.ts
export const tool: AppToolConfig = {
resource: 'project-detail', // renders the detail UI
title: 'Get Project Details',
description: 'Get full details for a specific project by ID.',
annotations: { readOnlyHint: true },
};
// src/tools/project-timeline.ts
export const tool: AppToolConfig = {
resource: 'project-timeline', // renders the timeline UI
title: 'Project Timeline',
description: 'Show the milestone timeline for a project.',
annotations: { readOnlyHint: true },
};
Each tool points to a different directory under src/resources/. The directory structure looks like this:
src/
resources/
project-list/
project-list.tsx # List of projects
project-detail/
project-detail.tsx # Single project detail card
project-timeline/
project-timeline.tsx # Gantt-style timeline view
tools/
list-projects.ts
get-project.ts
project-timeline.ts
Each resource component is a standalone React component that receives its tool’s structuredContent via useToolData(). The components don’t know about each other.
Multiple tools can also point to the same resource when they return different data for the same UI. For example, both search-tickets and get-ticket-by-id might point to a ticket-card resource, passing different data shapes that the component handles:
// src/resources/ticket-card/ticket-card.tsx
export function TicketCardResource() {
const { output: data } = useToolData<
unknown,
{ tickets: Ticket[] } | { ticket: Ticket }
>();
if (!data) return null;
// Handle both list and single-ticket responses
const tickets = 'tickets' in data ? data.tickets : [data.ticket];
return (
<SafeArea>
{tickets.map((ticket) => (
<TicketRow key={ticket.id} ticket={ticket} />
))}
</SafeArea>
);
}
When to use this pattern:
- Your connector covers multiple views of the same domain (list, detail, timeline, settings)
- Different user requests should render fundamentally different UIs
- You want to keep resource components focused and simple
- You have tools that share a similar data shape and can reuse a single component
Testing All Five Patterns
Every pattern above can be tested locally without a Claude account using sunpeak’s inspector. Create simulation files that define mock tool inputs and outputs, then run sunpeak dev to see your resources render with that data:
// src/resources/metrics-dashboard/simulations/q4-metrics.json
{
"title": "Q4 Performance Metrics",
"output": {
"period": "90d",
"pageViews": 142800,
"conversionRate": 0.034,
"revenue": 48200,
"errorRate": 0.008,
"sparkline": [4200, 4800, 5100, 4900, 5300, 5600, 5900]
}
}
For automated testing, use Vitest for unit tests and Playwright for end-to-end tests:
pnpm test # Unit tests
pnpm test:e2e # Playwright tests against the local inspector
Both run in CI/CD with GitHub Actions, so you can validate every pattern before deploying.
Picking the Right Pattern
If you are unsure which pattern fits, start with the simplest one that works:
- Data best consumed as text? Pattern 1 (read-only). Ship it in an afternoon.
- Data needs visual presentation? Pattern 2 (interactive). Add a resource component.
- Users need to create or modify records? Pattern 3 (CRUD). Add write tools with
destructiveHint. - Wrapping an existing API? Pattern 4 (API wrapper). One tool per endpoint.
- Multiple views of the same domain? Pattern 5 (multi-resource). Different tools, different UIs.
Most production connectors combine these patterns. A CRM connector is Pattern 3 + Pattern 5 (CRUD with multiple views). A SaaS dashboard connector is Pattern 4 + Pattern 2 (API wrapper with interactive UI). Start with the core pattern, then add complexity as your connector grows.
Get Started
pnpm add -g sunpeak && sunpeak new
Further Reading
- Claude Connectors tutorial - build and deploy a connector from scratch
- Designing Claude Connector tools - schemas, descriptions, and patterns for reliable tool calls
- Debugging Claude Connectors - fix common errors in development and production
- Claude Connectors vs Claude Apps - standard vs interactive connectors explained
- What are Claude Connectors - overview, data access, and auth
- Claude Connector OAuth authentication - when and how to wire up auth
- Claude Connector Directory submission - requirements for getting listed
- Claude Connector Framework - sunpeak overview
- sunpeak documentation - quickstart and API reference
Frequently Asked Questions
What types of Claude Connectors can I build?
You can build read-only data connectors that return text for Claude to reason over, interactive connectors that render UI inside the chat, CRUD connectors that let users create and update records through Claude, API wrapper connectors that expose any REST API as MCP tools, and multi-resource connectors with different UI views for different tools. All five patterns use the same MCP server architecture.
How do I decide between a standard and interactive Claude Connector?
Use a standard connector (text responses) when the data is best consumed as part of Claude conversation text, like search results, summaries, or simple lookups. Use an interactive connector (with UI) when the data benefits from visual presentation, like dashboards, charts, forms, status boards, or anything with actions the user should take directly.
Can a single Claude Connector have multiple tools and multiple UIs?
Yes. A single MCP server can register any number of tools, and each tool can reference a different resource component. A project management connector might have a search tool that shows a list view, a get-details tool that shows a detail card, and a create tool that shows a confirmation form. You define each as a separate tool file pointing to different resources.
How do I return structured data from a Claude Connector tool?
Return an object with a structuredContent key from your tool handler. The value should be a serializable object matching the shape your resource component expects. Your React resource component receives this data via the useToolData() hook from sunpeak. If your tool does not have a UI component, return a content array with type text instead.
What is the difference between structuredContent and text content in Claude Connector tools?
structuredContent is typed data passed to your resource component for rendering as UI via useToolData(). Text content (content with type text) goes directly into the Claude conversation as text that Claude can reference in its response. Interactive connectors use structuredContent. Standard connectors use text content. A single connector can mix both across different tools.
How do I test Claude Connector examples locally?
Run sunpeak dev to start a local inspector that replicates the Claude runtime at localhost:3000. Select Claude from the Host dropdown. The inspector loads simulation files (JSON mock data) so you can test every tool and resource without a paid Claude account or network calls. Use pnpm test for unit tests and pnpm test:e2e for Playwright end-to-end tests.
Can I build a Claude Connector that writes data back to my service?
Yes. CRUD connectors that create, update, or delete records are fully supported. Add destructiveHint: true to your tool annotations so Claude knows the operation modifies data. Claude may ask for user confirmation before calling destructive tools. Design write operations as small, focused tools with clear descriptions so Claude passes the right arguments.
Do Claude Connector examples work in ChatGPT too?
Yes. Claude Connectors are MCP servers, and ChatGPT also supports MCP. An interactive connector built with sunpeak renders in both Claude and ChatGPT without code changes. The same tool definitions, handlers, and resource components work across both hosts because both implement the MCP App standard.