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:8000 or set AGENT_URL in the frontend.
  • If Claude returns an authentication error, make sure ANTHROPIC_API_KEY is 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 zod

Bridge 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.

claude-agent-sdk-adapter.ts
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(),
    });
  });
}