Build Production AI Agents with the Claude Agent SDK and TypeScript

Anthropic didn't just build Claude — they built the infrastructure for anyone to create agents as capable as Claude Code. The Claude Agent SDK gives you the same agent loop, tools, and context management that power Claude Code, available as a programmable TypeScript (and Python) library.
In this tutorial, you'll go from zero to a production-ready AI agent that can read files, run commands, call custom tools, and orchestrate subagents — all in TypeScript.
Why the Claude Agent SDK? Unlike generic LLM frameworks, the Agent SDK gives you Claude Code's battle-tested agent loop out of the box. No need to implement retry logic, context management, or tool execution — it's all built in. You focus on what your agent does, not how it runs.
What You Will Learn
By the end of this tutorial, you will be able to:
- Set up a TypeScript project with the Claude Agent SDK
- Run an autonomous agent loop using the
queryfunction - Use built-in tools (Bash, Read, Write, Edit, Glob, Grep) without implementing them yourself
- Create custom MCP tools and connect them to your agent
- Configure permission modes for safe autonomous execution
- Build subagents for multi-agent orchestration
- Handle streaming messages and extract results
- Deploy agents with proper error handling and budget controls
Prerequisites
Before starting, ensure you have:
- Node.js 20+ installed (
node --version) - TypeScript 5.2+ knowledge (async iterators,
await using) - An Anthropic API key (get one at console.anthropic.com)
- Claude Code CLI installed (
npm install -g @anthropic-ai/claude-code) - A code editor — VS Code or Cursor recommended
Important: The Claude Agent SDK runs Claude Code under the hood. You need the Claude Code CLI installed and authenticated before using the SDK. Run claude in your terminal to verify it works.
How the Agent SDK Works
Traditional LLM libraries give you a single API call: prompt in, text out. The Agent SDK gives you a full agent loop — the same one that powers Claude Code:
Your Prompt → Agent Loop → Claude reasons about the task
↓
Claude calls a tool (Read, Bash, Edit, etc.)
↓
Tool executes and returns result
↓
Claude reasons about the result
↓
Claude calls another tool (or responds)
↓
... loop continues until task is complete ...
↓
Final result returned to you
The key insight: you don't implement the loop. The SDK handles tool execution, context management, token budgets, and error recovery. You provide the prompt and configuration.
Architecture
┌──────────────────────────────────────────┐
│ Your Application │
│ │
│ query({ prompt, options }) ──────┐ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ Claude Agent Loop │ │
│ │ │ │
│ │ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ Built-in │ │ Custom MCP │ │ │
│ │ │ Tools │ │ Tools │ │ │
│ │ │ │ │ │ │ │
│ │ │ - Bash │ │ - Your APIs │ │ │
│ │ │ - Read │ │ - Databases │ │ │
│ │ │ - Write │ │ - Services │ │ │
│ │ │ - Edit │ │ - Anything │ │ │
│ │ │ - Glob │ │ │ │ │
│ │ │ - Grep │ │ │ │ │
│ │ └──────────┘ └───────────────┘ │ │
│ │ │ │
│ │ ┌──────────────┐ │ │
│ │ │ Subagents │ │ │
│ │ │ (optional) │ │ │
│ │ └──────────────┘ │ │
│ └────────────────────────────────────┘ │
│ │
│ async for (message) ← streaming ◄─────│
└──────────────────────────────────────────┘
Step 1: Project Setup
Create a new TypeScript project and install the SDK:
mkdir claude-agent-project && cd claude-agent-project
npm init -y
npm install @anthropic-ai/claude-agent-sdk
npm install -D typescript @types/node tsxInitialize TypeScript:
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --outDir distUpdate your package.json to enable ES modules:
{
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"build": "tsc"
}
}Create the project structure:
mkdir srcStep 2: Your First Agent
Create src/index.ts — a simple agent that lists and analyzes files in a directory:
import { query } from "@anthropic-ai/claude-agent-sdk";
async function main() {
console.log("Starting agent...\n");
for await (const message of query({
prompt: "What files are in this directory? Give me a summary of the project structure.",
options: {
allowedTools: ["Bash", "Glob", "Read"],
maxTurns: 10,
},
})) {
// Handle different message types
if ("result" in message) {
console.log("\n--- Agent Result ---");
console.log(message.result);
}
}
}
main().catch(console.error);Run it:
npx tsx src/index.tsThat's it. The agent will:
- Receive your prompt
- Decide it needs to list files (calls
GloborBash) - Read the results
- Possibly read specific files for more context
- Return a structured summary
You didn't implement file reading, glob matching, or shell execution — the SDK provides all of that.
Step 3: Understanding Message Types
The query function returns an async iterator of messages. Let's build a more robust message handler:
import { query } from "@anthropic-ai/claude-agent-sdk";
async function main() {
for await (const message of query({
prompt: "Find all TypeScript files and count the total lines of code.",
options: {
allowedTools: ["Bash", "Glob", "Read"],
maxTurns: 15,
},
})) {
switch (message.type) {
case "assistant":
// Claude's reasoning or response text
const textBlocks = message.message.content
.filter((block: { type: string }) => block.type === "text")
.map((block: { type: string; text: string }) => block.text);
if (textBlocks.length > 0) {
console.log("[Claude]", textBlocks.join(""));
}
break;
case "tool_use":
// Claude is calling a tool
console.log(`[Tool Call] ${message.name}(${JSON.stringify(message.input).slice(0, 100)}...)`);
break;
case "tool_result":
// Tool execution result
console.log(`[Tool Result] ${message.content?.slice(0, 200)}...`);
break;
case "result":
if (message.subtype === "success") {
console.log("\n✓ Agent completed successfully");
console.log(message.result);
} else {
console.error("\n✗ Agent failed:", message.error);
}
break;
}
}
}
main().catch(console.error);Key Message Types
| Type | Description |
|---|---|
assistant | Claude's text response or reasoning |
tool_use | Claude requesting a tool call |
tool_result | Result from a tool execution |
result | Final result (success or error) |
Step 4: Built-in Tools
The Agent SDK comes with the same tools that power Claude Code. No implementation required — just allow them:
| Tool | What It Does |
|---|---|
Bash | Execute shell commands |
Read | Read file contents |
Write | Create or overwrite files |
Edit | Make targeted edits to files |
Glob | Find files by pattern |
Grep | Search file contents with regex |
Task | Launch subagent for parallel work |
WebFetch | Fetch and process web content |
WebSearch | Search the web |
Here's an agent that uses multiple built-in tools to refactor code:
import { query } from "@anthropic-ai/claude-agent-sdk";
async function refactorAgent(targetFile: string, instruction: string) {
for await (const message of query({
prompt: `Refactor the file ${targetFile}: ${instruction}`,
options: {
allowedTools: ["Read", "Edit", "Glob", "Grep"],
permissionMode: "acceptEdits", // Auto-approve file changes
maxTurns: 20,
},
})) {
if ("result" in message) {
return message.result;
}
}
}
// Usage
const result = await refactorAgent(
"src/utils.ts",
"Extract all helper functions into separate modules based on their domain"
);
console.log(result);Permission Modes
The permissionMode option controls what Claude can do without asking:
| Mode | Behavior |
|---|---|
"default" | Asks before any tool use |
"acceptEdits" | Auto-approves file reads and edits |
"bypassPermissions" | Auto-approves everything (use with caution) |
Security tip: In production, start with "default" or "acceptEdits". Only use "bypassPermissions" in sandboxed environments where the agent can't cause harm.
Step 5: Custom MCP Tools
The real power comes from connecting your own tools. The Agent SDK supports MCP (Model Context Protocol) servers — the same standard used by Claude Desktop and Cursor.
Creating an In-Process MCP Server
You can define tools directly in your TypeScript code using the SDK's createSdkMcpServer:
import { query } from "@anthropic-ai/claude-agent-sdk";
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk/mcp";
// Define custom tools
const analyticsServer = createSdkMcpServer({
name: "analytics",
version: "1.0.0",
tools: [
tool(
"get_page_views",
"Get page view analytics for a given URL path and date range",
{
type: "object",
properties: {
path: { type: "string", description: "URL path (e.g., /blog/my-post)" },
startDate: { type: "string", description: "Start date (YYYY-MM-DD)" },
endDate: { type: "string", description: "End date (YYYY-MM-DD)" },
},
required: ["path", "startDate", "endDate"],
},
async ({ path, startDate, endDate }) => {
// Your actual analytics API call here
const response = await fetch(
`https://api.analytics.example.com/views?path=${path}&start=${startDate}&end=${endDate}`
);
const data = await response.json();
return JSON.stringify(data);
}
),
tool(
"get_top_pages",
"Get the top N most visited pages",
{
type: "object",
properties: {
limit: { type: "number", description: "Number of pages to return" },
period: { type: "string", enum: ["day", "week", "month"], description: "Time period" },
},
required: ["limit", "period"],
},
async ({ limit, period }) => {
const response = await fetch(
`https://api.analytics.example.com/top?limit=${limit}&period=${period}`
);
const data = await response.json();
return JSON.stringify(data);
}
),
],
});
// Use with streaming input (required for MCP servers)
async function* generateMessages() {
yield {
type: "user" as const,
message: {
role: "user" as const,
content: "What are the top 10 most visited pages this month? Then analyze the trend for the #1 page over the last 30 days.",
},
};
}
for await (const message of query({
prompt: generateMessages(),
options: {
mcpServers: {
analytics: analyticsServer,
},
allowedTools: [
"mcp__analytics__get_page_views",
"mcp__analytics__get_top_pages",
],
maxTurns: 10,
},
})) {
if ("result" in message) {
console.log(message.result);
}
}Connecting External MCP Servers
You can also connect to any existing MCP server — like a database server, GitHub, or your own microservices:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "List open issues labeled 'bug' and summarize the most critical ones.",
options: {
mcpServers: {
github: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: {
GITHUB_TOKEN: process.env.GITHUB_TOKEN!,
},
},
},
allowedTools: ["mcp__github__*"], // Wildcard: allow all GitHub tools
maxTurns: 15,
},
})) {
if ("result" in message) {
console.log(message.result);
}
}SSE and HTTP Transports
For remote MCP servers, use SSE or HTTP transport:
options: {
mcpServers: {
"remote-api": {
type: "sse",
url: "https://mcp.example.com/sse",
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
},
},
}Step 6: System Prompts and Agent Behavior
Customize your agent's personality, constraints, and domain expertise:
import { query } from "@anthropic-ai/claude-agent-sdk";
const CODE_REVIEW_PROMPT = `You are a senior code reviewer specializing in TypeScript and React.
Your review process:
1. Read the changed files
2. Check for bugs, security issues, and performance problems
3. Verify TypeScript types are correct and complete
4. Ensure React best practices (hooks rules, key props, memo usage)
5. Provide a structured review with severity levels
Output format:
- 🔴 Critical: Must fix before merge
- 🟡 Warning: Should fix, but not blocking
- 🟢 Suggestion: Nice to have improvements
Be specific. Reference line numbers. Suggest fixes with code.`;
async function reviewCode(diffOrPath: string) {
for await (const message of query({
prompt: `Review this code:\n\n${diffOrPath}`,
options: {
systemPrompt: CODE_REVIEW_PROMPT,
allowedTools: ["Read", "Glob", "Grep"],
maxTurns: 25,
},
})) {
if ("result" in message) {
return message.result;
}
}
}Step 7: Subagents for Multi-Agent Orchestration
For complex tasks, you can define specialized subagents that the main agent can delegate to:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Analyze this project's test coverage, then write tests for any uncovered functions in src/utils/.",
options: {
allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "Task"],
agents: {
"test-analyzer": {
description: "Analyzes test coverage and identifies untested code paths",
tools: ["Bash", "Read", "Glob", "Grep"],
prompt: "You are a test coverage analyst. Run coverage tools, parse reports, and identify untested functions.",
model: "haiku", // Use a faster model for analysis
},
"test-writer": {
description: "Writes comprehensive unit tests for TypeScript functions",
tools: ["Read", "Write", "Edit", "Glob"],
prompt: "You are a test engineer. Write thorough unit tests using Vitest with proper mocking and edge case coverage.",
model: "sonnet",
},
},
maxTurns: 30,
},
})) {
if ("result" in message) {
console.log(message.result);
}
}How Subagents Work
When you define agents in the options, the main agent gets access to the Task tool. It can launch subagents by name, passing them a prompt. Each subagent:
- Runs in its own context with its own tool set
- Can use a different model (save costs with
haikufor simple tasks) - Returns results to the main agent for synthesis
This pattern is powerful for:
- Divide and conquer: Split analysis and implementation
- Cost optimization: Use cheaper models for simple subtasks
- Parallel work: Multiple subagents can run concurrently
- Separation of concerns: Each agent has focused expertise
Step 8: Budget and Safety Controls
Production agents need guardrails. The SDK provides several:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Migrate all JavaScript files to TypeScript with proper types.",
options: {
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
// Turn limits
maxTurns: 50, // Maximum tool calls before stopping
// Cost controls
maxBudgetUsd: 2.0, // Stop if cost exceeds $2.00
// Model selection
model: "claude-sonnet-4-5-20250929", // Use a specific model
// Tool restrictions
disallowedTools: ["WebSearch", "WebFetch"], // Block specific tools
// Working directory
cwd: "/path/to/project",
// Environment variables available to Bash tool
env: {
NODE_ENV: "development",
DATABASE_URL: process.env.DATABASE_URL!,
},
// Permission mode
permissionMode: "acceptEdits",
},
})) {
// Process messages
}The canUseTool Callback
For fine-grained control, implement a custom permission handler:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Clean up and optimize the codebase.",
options: {
allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
canUseTool: async (toolName, input) => {
// Block any destructive bash commands
if (toolName === "Bash") {
const command = (input as { command: string }).command;
const dangerous = ["rm -rf", "DROP TABLE", "git push --force"];
if (dangerous.some((d) => command.includes(d))) {
return { allowed: false, reason: "Destructive commands are not permitted" };
}
}
// Block writes to critical files
if (toolName === "Write" || toolName === "Edit") {
const filePath = (input as { file_path: string }).file_path;
const protected_paths = [".env", "package-lock.json", "prisma/schema.prisma"];
if (protected_paths.some((p) => filePath.includes(p))) {
return { allowed: false, reason: `Cannot modify protected file: ${filePath}` };
}
}
return { allowed: true };
},
},
})) {
if ("result" in message) {
console.log(message.result);
}
}Step 9: Building a Complete Project — AI Code Review Bot
Let's put everything together and build a practical AI code review bot that analyzes git diffs and provides structured feedback:
// src/review-bot.ts
import { query } from "@anthropic-ai/claude-agent-sdk";
interface ReviewResult {
summary: string;
issues: Array<{
severity: "critical" | "warning" | "suggestion";
file: string;
line?: number;
message: string;
suggestion?: string;
}>;
score: number;
}
const REVIEW_SYSTEM_PROMPT = `You are an expert code reviewer. Analyze git diffs and provide structured feedback.
Rules:
- Focus on bugs, security vulnerabilities, and performance issues
- Check for TypeScript type safety
- Verify error handling is comprehensive
- Look for potential race conditions in async code
- Suggest improvements only when they provide clear value
Always end your review with a JSON block in this exact format:
\`\`\`json
{
"summary": "One paragraph summary",
"issues": [
{
"severity": "critical|warning|suggestion",
"file": "path/to/file.ts",
"line": 42,
"message": "Description of the issue",
"suggestion": "How to fix it"
}
],
"score": 85
}
\`\`\`
Score: 0-100 where 100 is perfect code.`;
async function reviewPullRequest(baseBranch: string = "main"): Promise<ReviewResult | null> {
let result: string | null = null;
for await (const message of query({
prompt: `Review the current git diff against ${baseBranch}.
Read the changed files for full context, then provide a structured code review.`,
options: {
systemPrompt: REVIEW_SYSTEM_PROMPT,
allowedTools: ["Bash", "Read", "Glob", "Grep"],
maxTurns: 30,
maxBudgetUsd: 1.0,
permissionMode: "acceptEdits",
},
})) {
if ("result" in message && message.subtype === "success") {
result = message.result;
}
}
if (!result) return null;
// Extract JSON from the result
const jsonMatch = result.match(/```json\n([\s\S]*?)\n```/);
if (jsonMatch) {
return JSON.parse(jsonMatch[1]) as ReviewResult;
}
return null;
}
// Run the review
async function main() {
console.log("Starting code review...\n");
const review = await reviewPullRequest();
if (review) {
console.log(`\nReview Score: ${review.score}/100`);
console.log(`Summary: ${review.summary}\n`);
for (const issue of review.issues) {
const icon =
issue.severity === "critical" ? "🔴" :
issue.severity === "warning" ? "🟡" : "🟢";
console.log(`${icon} [${issue.file}${issue.line ? `:${issue.line}` : ""}] ${issue.message}`);
if (issue.suggestion) {
console.log(` Fix: ${issue.suggestion}`);
}
}
}
}
main().catch(console.error);Run it against your current branch:
npx tsx src/review-bot.tsStep 10: V2 Sessions (Preview)
The Agent SDK also includes a v2 preview API with explicit session management, useful for multi-turn conversations:
import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
async function interactiveSession() {
await using session = unstable_v2_createSession({
model: "claude-sonnet-4-5-20250929",
});
// First turn
await session.send("Hello! What TypeScript project am I in?");
for await (const msg of session.stream()) {
if (msg.type === "assistant") {
const text = msg.message.content
.filter((block: { type: string }) => block.type === "text")
.map((block: { type: string; text: string }) => block.text)
.join("");
console.log(text);
}
}
// Second turn — session remembers context
await session.send("Now list the main dependencies from package.json");
for await (const msg of session.stream()) {
if (msg.type === "assistant") {
const text = msg.message.content
.filter((block: { type: string }) => block.type === "text")
.map((block: { type: string; text: string }) => block.text)
.join("");
console.log(text);
}
}
}
interactiveSession().catch(console.error);Note: The v2 session API is in preview (unstable_v2_createSession). The API may change in future releases. For production use, the query function is the stable API.
Testing Your Agent
Create a simple test to verify your agent works:
// src/test-agent.ts
import { query } from "@anthropic-ai/claude-agent-sdk";
async function testAgent() {
const tests = [
{
name: "Basic file listing",
prompt: "List the files in the current directory",
tools: ["Glob"],
expectResult: true,
},
{
name: "Code analysis",
prompt: "Read package.json and tell me the project name",
tools: ["Read"],
expectResult: true,
},
];
for (const test of tests) {
console.log(`\nTest: ${test.name}`);
let gotResult = false;
for await (const message of query({
prompt: test.prompt,
options: {
allowedTools: test.tools,
maxTurns: 5,
},
})) {
if ("result" in message) {
gotResult = true;
console.log(` ✓ Result: ${message.result.slice(0, 100)}...`);
}
}
if (gotResult === test.expectResult) {
console.log(` ✓ PASSED`);
} else {
console.log(` ✗ FAILED`);
}
}
}
testAgent().catch(console.error);Troubleshooting
"Claude Code CLI not found"
The Agent SDK requires Claude Code to be installed globally:
npm install -g @anthropic-ai/claude-code
claude --version # Verify installation"Permission denied" errors
If the agent can't execute tools, check your permissionMode and allowedTools:
// Ensure the tools you need are in the allowed list
options: {
allowedTools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
permissionMode: "acceptEdits",
}Agent runs too long
Set maxTurns and maxBudgetUsd to prevent runaway execution:
options: {
maxTurns: 20, // Stop after 20 tool calls
maxBudgetUsd: 0.50, // Stop if cost exceeds $0.50
}MCP server connection fails
For stdio servers, ensure the command exists and works standalone:
# Test the MCP server directly
npx -y @modelcontextprotocol/server-githubFor SSE servers, verify the URL is accessible and CORS headers are correct.
Next Steps
Now that you can build agents with the Claude Agent SDK, here are some ideas to explore:
- Build a CI/CD agent that reviews PRs, runs tests, and posts results to GitHub
- Create a documentation agent that reads your codebase and generates API docs
- Build a migration agent that converts JavaScript to TypeScript with proper types
- Integrate with your own MCP servers — connect databases, APIs, and internal tools
- Explore the v2 session API for building interactive chat applications
Related Tutorials
- Build Your First MCP Server with TypeScript — Create the tools your agents will use
- Building AI Agents with the ReAct Pattern — Understand the agent loop at a lower level
- Building Multi-Agent AI Systems with n8n — Visual multi-agent orchestration
Conclusion
The Claude Agent SDK removes the hardest parts of building AI agents — the loop management, tool execution, context handling, and error recovery. What used to require hundreds of lines of custom code is now a single query call with the right configuration.
You learned how to:
- Set up a TypeScript project with the Agent SDK
- Run agents using the
queryfunction with built-in tools - Create custom tools using MCP servers
- Configure permissions for safe autonomous execution
- Build subagents for multi-agent orchestration
- Add guardrails with budget limits and custom permission handlers
- Build a practical project — an AI code review bot
The shift from "building LLM wrappers" to "programming autonomous agents" is the defining developer trend of 2026. With the Claude Agent SDK, you're now equipped to build agents that don't just answer questions — they get work done.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Building AI Agents from Scratch with TypeScript: Master the ReAct Pattern Using the Vercel AI SDK
Learn how to build AI agents from the ground up using TypeScript. This tutorial covers the ReAct pattern, tool calling, multi-step reasoning, and production-ready agent loops with the Vercel AI SDK.

Build Your First MCP Server with TypeScript: Tools, Resources, and Prompts
Learn how to build a production-ready MCP server from scratch using TypeScript. This hands-on tutorial covers tools, resources, prompts, stdio transport, and connecting to Claude Desktop and Cursor.

WordPress MCP Adapter: Making Your Site AI-Agent Ready
Learn how to install and configure the WordPress MCP Adapter to make your WordPress site accessible to AI agents in Cursor, Claude Desktop, and other MCP-compatible tools. Complete step-by-step guide with practical examples.