Quickstart
Run a Claude Agent SDK TypeScript agent behind CopilotKit.
This quickstart gives you two working paths:
- Start from scratch to scaffold the full Claude Agent SDK TypeScript showcase.
- Use an existing agent to expose your own Claude Agent SDK process over AG-UI and connect it to a React app.
Prerequisites#
Before you begin, you'll need the following:
- An Anthropic API key
- Node.js 20+
- Your favorite package manager
Getting started#
Choose your starting point#
Verify the integration#
Open http://localhost:3000 and ask:
Tell me in one sentence what this app can do.If the chat streams a Claude response, CopilotKit is connected to your Claude Agent SDK TypeScript agent.
Troubleshooting
- If the runtime cannot reach the agent, check that the agent server is running on
http://localhost:8000or setAGENT_URLin the frontend. - If Claude returns an authentication error, make sure
ANTHROPIC_API_KEYis available to the agent server process. - If a custom tool call stalls, add the backend tool bridge shown below instead of leaving
tools: [].
Backend tools and state#
The minimal server above is enough for text chat. To support backend tools, shared state snapshots, and richer demos, expose your tool schemas through the Claude SDK MCP bridge and emit AG-UI state events:
Install the Claude Agent SDK packages
npm install @ag-ui/core @ag-ui/encoder @ag-ui/claude-agent-sdk @anthropic-ai/claude-agent-sdk@^0.2.58 @anthropic-ai/sdk zodBridge Claude Agent SDK to AG-UI
Use ClaudeAgentAdapter from @ag-ui/claude-agent-sdk. The adapter
receives the AG-UI run input, emits AG-UI events back to CopilotKit, and can
expose backend tools through an in-process Claude SDK MCP server.
export async function runWithClaudeAgentSdk({
input,
emit,
runId,
threadId,
systemPrompt,
toolSchemas,
initialState,
model,
executeTool,
forwardedHeaders,
}: {
input: RunAgentInput;
emit: Emit;
runId: string;
threadId: string;
systemPrompt: string;
toolSchemas: Anthropic.Tool[];
initialState: Record<string, unknown>;
model: string;
executeTool: ExecuteTool;
forwardedHeaders?: Record<string, string>;
}): Promise<void> {
let state = { ...initialState };
const pendingStateSnapshots: Record<string, unknown>[] = [];
const backendToolServer = buildBackendToolServer({
toolSchemas,
emit,
getState: () => state,
setState: (nextState) => {
state = nextState;
pendingStateSnapshots.push(state);
},
executeTool,
});
const adapter = new ClaudeAgentAdapter({
agentId: "claude-sdk-typescript",
model: normalizeClaudeAgentSdkModel(model),
systemPrompt,
tools: [],
mcpServers: backendToolServer.mcpServers,
allowedTools: backendToolServer.allowedTools,
permissionMode: "dontAsk",
maxTurns: 10,
});
if (forwardedHeaders && Object.keys(forwardedHeaders).length > 0) {
adapter.headers = forwardedHeaders;
}
const runInput: RunAgentInput = {
...input,
runId,
threadId,
state: input.state ?? initialState,
};
await new Promise<void>((resolve) => {
adapter.run(runInput).subscribe({
next: (event) => {
if (event.type === EventType.TOOL_CALL_RESULT) {
const snapshot = pendingStateSnapshots.shift();
if (snapshot) {
emit({ type: EventType.STATE_SNAPSHOT, snapshot });
}
}
emit(event);
},
error: (error) => {
const message =
error instanceof Error ? error.stack || error.message : String(error);
emit({ type: EventType.RUN_ERROR, runId, threadId, message });
resolve();
},
complete: () => resolve(),
});
});
}