Claude Connector Examples: 5 Patterns for Building Real Connectors (August 2026)

Five practical Claude Connector patterns with code examples.
TL;DR: Most production Claude Connectors combine five patterns: read-only retrieval, an optional MCP App UI, narrow write tools, a REST API adapter, and multiple resources for distinct views. Pick the smallest pattern that completes the user’s job, keep tool results compact, and test protocol behavior separately from UI behavior.
A Claude Connector is a remote MCP server that gives Claude tools, prompts, resources, or an interactive MCP App. Claude currently supports Streamable HTTP and legacy HTTP+SSE for custom connectors, but its docs mark HTTP+SSE for deprecation. New deployments should use Streamable HTTP.
The MCP ecosystem has also changed since the original version of this guide. MCP Apps now has a portable tool-to-resource contract, and the July 2026 MCP core is stateless. Those changes affect how you structure UI tools, stateful workflows, tests, and fallbacks.
What Claude supports now
Claude’s custom connector surface supports tools, prompts, resources, text and image tool results, and text and binary resources. It does not yet support resource subscriptions, sampling, or advanced and draft capabilities. The Claude API MCP connector is a different surface: it currently exposes tool calls only, requires a public HTTPS server, and uses the mcp-client-2025-11-20 beta.
Plan around the published host limits:
| Surface | Tool result limit | Tool timeout |
|---|---|---|
| Claude.ai and Claude Desktop | About 150,000 characters | 300 seconds |
| Claude Code | 25,000 tokens by default | Configurable |
These are ceilings, not targets. A smaller response gives the model less irrelevant context and makes retries cheaper. Rank, filter, and paginate at the server.
The contract shared by all five patterns
Every good connector starts with a tight contract:
schemadescribes the arguments the model may send.outputSchemadescribesstructuredContentwhen the tool returns structured data.contentis the model-readable and text-only fallback channel.structuredContentis stable data for an MCP App and other structured clients._metacarries host or app metadata that should not enter model context.
For an interactive tool, the MCP Apps specification associates the tool with a ui:// resource through nested _meta.ui.resourceUri. The resource uses text/html;profile=mcp-app. sunpeak generates that protocol metadata when a tool’s resource field points to a resource directory.
Pattern 1: Read-only data connector
Use a text-first tool when Claude’s answer is the interface. Documentation search, account status, policy lookup, and log summaries usually do not need a custom UI.
// 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 for policies, runbooks, API details, or engineering instructions.',
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
};
export const schema = {
query: z.string().min(2).describe('Keywords or a natural-language question'),
limit: z.number().int().min(1).max(10).default(5),
};
type Args = z.infer<z.ZodObject<typeof schema>>;
export default async function (args: Args, extra: ToolHandlerExtra) {
const results = await searchDocuments(args.query, args.limit, extra.signal);
return {
content: [
{
type: 'text' as const,
text: results.map((doc) => `${doc.title}\n${doc.summary}\n${doc.url}`).join('\n\n'),
},
],
};
}
This tool accepts a user intent rather than raw search-engine parameters. The server owns ranking and limits the result count. It also passes the MCP cancellation signal downstream, so abandoned calls can stop work.
Add a cursor when users need more than one page. Do not return every match or a full document collection in one call.
Pattern 2: Interactive connector with an MCP App UI
Add UI when users need to scan, compare, or act on structured data. Tables, charts, maps, approval cards, and timelines are good candidates.
// 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 Product Metrics',
description: 'Show traffic, conversion, revenue, and error rate for a time range.',
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
};
export const schema = {
period: z.enum(['today', '7d', '30d', '90d']),
};
export const outputSchema = {
period: z.string(),
pageViews: z.number(),
conversionRate: z.number(),
revenue: z.number(),
errorRate: z.number(),
};
type Args = z.infer<z.ZodObject<typeof schema>>;
export default async function (args: Args, extra: ToolHandlerExtra) {
const metrics = await fetchMetrics(args.period, extra.signal);
const output = { period: args.period, ...metrics };
return {
content: [
{
type: 'text' as const,
text: `For ${args.period}: ${metrics.pageViews} views, ${metrics.conversionRate}% conversion, $${metrics.revenue} revenue, and ${metrics.errorRate}% errors.`,
},
],
structuredContent: output,
};
}
// src/resources/metrics-dashboard/metrics-dashboard.tsx
import { SafeArea, useToolData } from 'sunpeak';
import type { ResourceConfig } from 'sunpeak';
type Metrics = {
period: string;
pageViews: number;
conversionRate: number;
revenue: number;
errorRate: number;
};
export const resource: ResourceConfig = {
title: 'Metrics Dashboard',
description: 'Compact product metrics dashboard',
mimeType: 'text/html;profile=mcp-app',
};
export function MetricsDashboardResource() {
const { output } = useToolData<unknown, Metrics>();
if (!output) return null;
return (
<SafeArea className="grid grid-cols-2 gap-3 p-4">
<Metric label="Page views" value={output.pageViews.toLocaleString()} />
<Metric label="Conversion" value={`${output.conversionRate}%`} />
<Metric label="Revenue" value={`$${output.revenue.toLocaleString()}`} />
<Metric label="Error rate" value={`${output.errorRate}%`} />
</SafeArea>
);
}
In a real project, export the shared data type from a neutral module instead of importing a server tool into the UI bundle. Keep content useful because MCP Apps is negotiated. A host that does not render the resource can still show the text result.
Declare every external origin in the resource’s _meta.ui.csp. The MCP Apps default blocks undeclared network requests and remote assets.
Pattern 3: Confirmed write-action connector
Writes need smaller tools than reads. Split the workflow into preview and commit so the user can see the exact target and effect before data changes.
// src/tools/resolve-ticket.ts
import { z } from 'zod';
import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
export const tool: AppToolConfig = {
title: 'Resolve Support Ticket',
description: 'Resolve one ticket after the user approves the ticket ID and note.',
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
},
_meta: { ui: { visibility: ['app'] } },
};
export const schema = {
ticketId: z.string().min(1),
resolutionNote: z.string().min(10),
idempotencyKey: z.string().uuid(),
};
type Args = z.infer<z.ZodObject<typeof schema>>;
export default async function (args: Args, extra: ToolHandlerExtra) {
const ticket = await resolveTicketOnce(args, extra.authInfo?.token);
return {
content: [{ type: 'text' as const, text: `Resolved ${ticket.id}: ${ticket.title}.` }],
structuredContent: { status: ticket.status, ticket },
};
}
An MCP App review resource can call this app-only tool after confirmation. visibility: ['app'] keeps the commit tool out of the model’s tool list while allowing the app on the same server connection to call it. If your product allows direct model-initiated writes, include model visibility and rely on the host’s confirmation policy as well.
The idempotency key makes a retry return the first result instead of repeating the action. The handler returns the stored final state, not the requested state, which exposes permission checks, conflicts, and server-side changes.
Pattern 4: Focused REST API wrapper
Most teams already have an HTTP API. The connector should translate user intents into a small set of product operations, not expose a generic method, path, and body tool.
// 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: 'Get one invoice by number, including status, amount due, and due date.',
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: true,
},
};
export const schema = {
invoiceNumber: z.string().regex(/^INV-[0-9]+$/),
};
export const outputSchema = {
status: z.enum(['found', 'not_found']),
invoiceNumber: z.string(),
amountDue: z.number().optional(),
dueDate: z.string().optional(),
};
type Args = z.infer<z.ZodObject<typeof schema>>;
export default async function (args: Args, extra: ToolHandlerExtra) {
const response = await fetch(
`https://api.example.com/invoices/${encodeURIComponent(args.invoiceNumber)}`,
{
headers: { Authorization: `Bearer ${extra.authInfo?.token}` },
signal: extra.signal,
}
);
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) {
return {
isError: true,
content: [
{ type: 'text' as const, text: 'The billing service could not return this invoice.' },
],
};
}
const invoice = await response.json();
const output = {
status: 'found' as const,
invoiceNumber: invoice.number,
amountDue: invoice.amountDue,
dueDate: invoice.dueDate,
};
return {
content: [
{
type: 'text' as const,
text: `Invoice ${output.invoiceNumber} has $${output.amountDue} due on ${output.dueDate}.`,
},
],
structuredContent: output,
};
}
Authenticate at the MCP boundary, pass the user’s scoped token upstream, and never log it. Map a valid empty result such as 404 to structured data. Return isError: true for a tool execution failure the model can recover from, and reserve protocol errors for malformed MCP requests or transport failures.
Add upstream timeouts, rate-limit handling, and safe retries. Return only fields the model or app needs.
Pattern 5: Multi-resource workflow
Use multiple resources when a domain has genuinely different views. A project connector might have a list, detail, and timeline tool:
export const listProjects: AppToolConfig = {
resource: 'project-list',
title: 'List Projects',
description: 'List projects with status, owner, due date, and risk.',
annotations: { readOnlyHint: true, destructiveHint: false },
};
export const getProject: AppToolConfig = {
resource: 'project-detail',
title: 'Get Project Details',
description: 'Get milestones, blockers, and recent activity for one project ID.',
annotations: { readOnlyHint: true, destructiveHint: false },
};
export const getTimeline: AppToolConfig = {
resource: 'project-timeline',
title: 'Get Project Timeline',
description: 'Show milestone timing and dependencies for one project ID.',
annotations: { readOnlyHint: true, destructiveHint: false },
};
Keep one resource entry point per view and share ordinary components and schemas beneath them. This makes empty, error, compact, fullscreen, light, and dark states testable without one resource accumulating every workflow.
The July 2026 MCP core removes protocol-level sessions. If a workflow needs state across calls, return an explicit handle from one tool and require it in the next tool’s schema. A visible handle works across stateless server instances and lets the model pass state intentionally.
Choosing a pattern
| User job | Start with | Add next when needed |
|---|---|---|
| Find a policy or runbook | Read-only data tool | Cursor pagination |
| Compare metrics | MCP App UI | App-only refresh tool |
| Resolve a ticket | Preview plus write tool | Idempotency and conflict UI |
| Query a billing system | REST API wrapper | Cache and rate-limit policy |
| Browse projects and timelines | Multiple resources | Explicit workflow handles |
Most production connectors combine patterns. The useful boundary is one user intent per tool and one visual job per resource.
Test the protocol and UI separately
Protocol tests catch bad schemas, annotations, auth, and result shapes. UI tests catch host context, responsive layout, themes, and interaction bugs. You need both.
Current sunpeak simulations use tool, toolInput, and toolResult:
{
"tool": "get-invoice",
"toolInput": { "invoiceNumber": "INV-1042" },
"toolResult": {
"content": [{ "type": "text", "text": "Invoice INV-1042 has $4200 due on 2026-08-31." }],
"structuredContent": {
"status": "found",
"invoiceNumber": "INV-1042",
"amountDue": 4200,
"dueDate": "2026-08-31"
}
}
}
Then test the real server and rendered app through separate fixtures:
import { expect, test } from 'sunpeak/test';
test('invoice contract and card', async ({ mcp, inspector }) => {
const raw = await mcp.callTool('get-invoice', { invoiceNumber: 'INV-1042' });
expect(raw.isError).toBeFalsy();
expect(raw.structuredContent).toMatchObject({ invoiceNumber: 'INV-1042' });
const rendered = await inspector.renderTool(
'get-invoice',
{ invoiceNumber: 'INV-1042' },
{ theme: 'dark' }
);
await expect(rendered.app().getByText('INV-1042')).toBeVisible();
});
Passing input to renderTool calls the real server. Omitting input uses a matching simulation fixture when one exists. sunpeak’s Playwright configuration runs the same E2E test against its replicated Claude and ChatGPT host runtimes, so cross-host checks do not consume model credits.
Production checklist
- Use Streamable HTTP for new remote connectors.
- Keep tool names, descriptions, schemas, output schemas, and annotations specific.
- Return compact text fallbacks for UI tools.
- Separate preview, confirmation, and commit for sensitive writes.
- Use explicit workflow handles instead of transport session state.
- Test OAuth denial, expiry, refresh, and missing scopes from a clean browser session.
- Test empty, error, permission, timeout, retry, and rate-limit states.
- Check MCP App resources in compact and fullscreen modes, both themes, and narrow layouts.
- Declare resource CSP origins and test links through host APIs.
- Run one real Claude smoke test against the deployed HTTPS endpoint.
sunpeak can build these patterns as a file-based MCP App project, or inspect and test an existing TypeScript, Python, Go, or other MCP server. Its local runtime replicas, simulation fixtures, protocol assertions, and Playwright helpers make the same server states repeatable in development and CI.
Start with the connector’s smallest useful tool. Add structured UI when the user needs to inspect data, add writes only with a clear approval boundary, and split resources when views have different jobs. That keeps the connector easier to understand, test, and operate as its tool set grows.
Get Started
npx sunpeak newFurther 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 - result channels and pagination
- Move a Claude Connector from SSE to Streamable HTTP
- Test MCP Apps across supported host runtimes with sunpeak
- sunpeak simulation fixtures for deterministic MCP App states
- Claude custom connector requirements and current limits
- Official MCP Apps overview
- MCP Apps specification for tool and resource contracts
- MCP tools specification for schemas, annotations, and results
Frequently Asked Questions
What are the most common Claude Connector examples?
Five patterns cover most projects: read-only search, interactive MCP App UI, confirmed write actions, focused REST API wrappers, and multi-resource workflows. A production connector often combines two or three of them.
Should a Claude Connector return content or structuredContent?
Return concise text content for the model and text-only hosts. Return structuredContent for stable data that an MCP App renders. If a tool declares outputSchema, its structuredContent must match that schema. Many interactive connectors should return both channels.
Can Claude Connectors render interactive UI?
Yes. MCP Apps associate a tool with a ui:// resource whose MIME type is text/html;profile=mcp-app. A compatible host reads that resource and renders it in a sandboxed iframe. Keep a text fallback because MCP Apps support is negotiated and not every client renders UI.
How should I design write tools in a Claude Connector?
Separate preview from commit, require the exact target and an idempotency key, use honest tool annotations, and return the final stored state. An MCP App can call an app-only commit tool after the user confirms the preview.
Can a Claude Connector also work in ChatGPT?
A connector can work across hosts when it uses portable MCP tool, resource, and MCP Apps contracts. Keep host-specific APIs behind capability checks and test the same server in each host runtime you support.
What Claude limits should connector developers plan for?
Claude documents an approximate 150,000-character tool result limit and a 300-second tool timeout for Claude.ai and Claude Desktop. Claude Code has a configurable 25,000-token result limit and configurable timeout. Small, paginated results are still easier for models and users to handle.
How do I test Claude Connector examples locally?
Test protocol behavior and rendered UI separately. With sunpeak, simulation JSON supplies deterministic toolInput, toolResult, hostContext, and serverTools data. Playwright tests can then run against replicated Claude and ChatGPT runtimes without using paid host accounts or model credits.
What should I check before publishing a Claude Connector?
Check tool selection, schemas, annotations, OAuth, empty and error states, result size, UI accessibility, light and dark themes, narrow layouts, and retry behavior. Also run one HTTPS smoke test in the real Claude surface you plan to support.