Build Production AI Agents with the Claude Agent SDK and TypeScript

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

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 query function
  • 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 tsx

Initialize TypeScript:

npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --outDir dist

Update your package.json to enable ES modules:

{
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts",
    "build": "tsc"
  }
}

Create the project structure:

mkdir src

Step 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.ts

That's it. The agent will:

  1. Receive your prompt
  2. Decide it needs to list files (calls Glob or Bash)
  3. Read the results
  4. Possibly read specific files for more context
  5. 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

TypeDescription
assistantClaude's text response or reasoning
tool_useClaude requesting a tool call
tool_resultResult from a tool execution
resultFinal 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:

ToolWhat It Does
BashExecute shell commands
ReadRead file contents
WriteCreate or overwrite files
EditMake targeted edits to files
GlobFind files by pattern
GrepSearch file contents with regex
TaskLaunch subagent for parallel work
WebFetchFetch and process web content
WebSearchSearch 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:

ModeBehavior
"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 haiku for 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.ts

Step 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-github

For 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

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:

  1. Set up a TypeScript project with the Agent SDK
  2. Run agents using the query function with built-in tools
  3. Create custom tools using MCP servers
  4. Configure permissions for safe autonomous execution
  5. Build subagents for multi-agent orchestration
  6. Add guardrails with budget limits and custom permission handlers
  7. 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.


Want to read more tutorials? Check out our latest tutorial on Making Outgoing Calls with Twilio Voice and OpenAI.

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

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.

25 min read·